Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,9 @@ dependencies = [
"channels[daphne]>=4.3.2,<5",
"uvicorn[standard]>=0.52.1",
"channels-redis>=4,<5",
"django-flexible-reports>=0.4.2",
# >=0.5.0 wnosi Report.set_order_by() — bez niego wybór sortowania w
# formularzu raportów (FD467) nie ma jak nadpisać ColumnOrder tabeli.
"django-flexible-reports>=0.5.0",
"django-dsl>=0.1.15",
"django-group-by==0.3.1",
"django-columns==0.1.0",
Expand Down
6 changes: 6 additions & 0 deletions src/bpp/newsfragments/fd467.feature.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Formularz raportów (autora, jednostki, wydziału, uczelni) ma nowe pole
"Sortowanie". Obok dotychczasowego porządku (rok malejąco, potem opis
bibliograficzny) można wybrać uporządkowanie alfabetyczne wg nazwisk autorów —
przydatne przy spisach do sprawozdań rocznych. Wybór dotyczy także eksportu do
MS Word i MS Excel. Domyślną wartość pola dla swojej instalacji można ustawić
w panelu domyślnych wartości formularzy.
25 changes: 22 additions & 3 deletions src/nowe_raporty/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
from bpp.models.cache import Rekord
from bpp.util import formdefaults_html_after, formdefaults_html_before, year_last_month

from . import sortowanie as sortowanie_raportu
from .models import DefinicjaRaportu


Expand Down Expand Up @@ -51,6 +52,15 @@ class BaseRaportForm(forms.Form):
label="Format wyjściowy", choices=OUTPUT_FORMATS, required=True
)

# FD467: porządek spisu. Nie jest to filtr, więc stoi w głównym fieldsecie
# obok formatu wyjściowego, a nie w "Opcjach zaawansowanych".
sortowanie = forms.ChoiceField(
label="Sortowanie",
choices=sortowanie_raportu.WYBORY,
initial=sortowanie_raportu.DOMYSLNE,
required=True,
)

# Wspólny default; podklasy per-poziom mogą nadpisać label/help_text.
tylko_z_jednostek_uczelni = forms.BooleanField(
initial=True,
Expand All @@ -71,15 +81,21 @@ class BaseRaportForm(forms.Form):
required=False, label="Tylko prace punktowane (pkt MNiSW > 0)"
)

# nazwy pól zaawansowanych przekazywanych w querystringu do widoku generuj
POLA_ZAAWANSOWANE = [
# Nazwy pól przekazywanych w querystringu do widoku generującego. Lista
# steruje WYŁĄCZNIE przekazywaniem — o wyglądzie decyduje ``Layout`` niżej,
# dlatego jest tu ``sortowanie``, które w układzie stoi zupełnie gdzie
# indziej niż filtry zaawansowane. (Stąd nazwa ``POLA_PRZEKAZYWANE``, a nie
# dawna ``POLA_ZAAWANSOWANE``: ta druga sugerowała związek z sekcją "Opcje
# zaawansowane", którego nigdy nie było.)
POLA_PRZEKAZYWANE = [
"punkty_mnisw_od",
"punkty_mnisw_do",
"if_od",
"if_do",
"punktacja_wewnetrzna_od",
"punktacja_wewnetrzna_do",
"tylko_punktowane",
"sortowanie",
]

def clean(self):
Expand Down Expand Up @@ -140,7 +156,10 @@ def __init__(self, *args, request=None, **kwargs):
Column("od_roku", css_class="large-6 medium-6 small-12"),
Column("do_roku", css_class="large-6 medium-6 small-12"),
),
Row(Column("_export")),
Row(
Column("_export", css_class="large-6 medium-6 small-12"),
Column("sortowanie", css_class="large-6 medium-6 small-12"),
),
Row(Column("tylko_z_jednostek_uczelni")),
# "Opcje zaawansowane" jako rozwijana sekcja WEWNĄTRZ fieldsetu
# (analogicznie do "Filtry ..." na ranking-autorow). Natywny
Expand Down
51 changes: 51 additions & 0 deletions src/nowe_raporty/sortowanie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
"""Porządki sortowania raportu — wybierane przez użytkownika w formularzu.

Domyślnie kolejność wierszy jest własnością tabeli ``flexible_reports.Table``
(inline ``ColumnOrder``: rok malejąco, potem opis). FD467 poprosił o spis
uporządkowany wg nazwisk autorów — czyli o wybór w czasie generowania, nie o
drugą tabelę. Realizuje to ``Report.set_order_by()``, który nadpisuje
``ColumnOrder`` na jeden render.

Moduł jest wspólnym źródłem prawdy dla formularza (etykiety wyborów) i widoku
(pola ORM), analogicznie do ``poziomy.py``. Rozdzielenie ich groziłoby cichym
rozjechaniem: formularz oferowałby wariant, którego widok nie zna, i raport po
prostu wychodziłby posortowany domyślnie.
"""

DOMYSLNE = "domyslne"
WG_AUTOROW = "autorzy"

WYBORY = [
(DOMYSLNE, "Rok (malejąco), potem opis bibliograficzny"),
(WG_AUTOROW, "Nazwiska autorów (alfabetycznie)"),
]

# ``opis_bibliograficzny_autorzy_cache`` to ArrayField ["Nazwisko Imiona", ...]
# w kolejności autorstwa, a PostgreSQL porównuje tablice element po elemencie —
# sortowanie po nim daje więc porządek wg nazwiska PIERWSZEGO autora, z
# kolejnymi autorami jako rozstrzygnięciem remisu.
#
# To jedyne miejsce, w którym "nazwisko pierwszego autora" jest sortowalnym
# skalarem. Przez relację (``autorzy__autor__nazwisko``) sortować się NIE da:
# JOIN po relacji wielowartościowej zduplikowałby wiersze, a ``Column.clean()``
# i tak odrzuca taką ścieżkę w dot-notation.
#
# ``tytul_oryginalny_sort`` na końcu nie jest ozdobą: bez niego dwie prace tego
# samego autora z tego samego roku mają kolejność niezdeterminowaną, więc ten
# sam raport wygenerowany dwa razy potrafiłby się różnić.
WG_AUTOROW_POLA = (
"opis_bibliograficzny_autorzy_cache",
"rok",
"tytul_oryginalny_sort",
)

# Tylko warianty nadpisujące. Brak klucza (w tym ``DOMYSLNE``) = zostaw
# ``ColumnOrder`` tabeli w spokoju.
PORZADKI = {
WG_AUTOROW: WG_AUTOROW_POLA,
}


def pola_dla(wybor):
"""Pola ORM dla wariantu z querystringa, albo ``None`` gdy bez nadpisania."""
return PORZADKI.get(wybor)
133 changes: 133 additions & 0 deletions src/nowe_raporty/tests/test_sortowanie.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""Wybór porządku sortowania raportu (FD467).

Raport ma domyślnie kolejność zapisaną w ``ColumnOrder`` tabeli (rok malejąco,
potem opis). Zgłoszenie FD467 prosi o spis uporządkowany wg nazwisk autorów —
stąd wybór w formularzu, przekazywany querystringiem do widoku generującego.
"""

import pytest
from model_bakery import baker

from nowe_raporty import sortowanie
from nowe_raporty.forms import form_class_dla
from nowe_raporty.models import DefinicjaRaportu
from nowe_raporty.seeding import seed_default_reports
from nowe_raporty.views import RaportGenerujView, _redirect_do_generuj

from bpp.models.autor import Autor
from bpp.models.cache import Rekord
from bpp.models.struktura import Jednostka
from bpp.models.wydawnictwo_ciagle import Wydawnictwo_Ciagle


@pytest.fixture
def prace_dwoch_autorow(typy_odpowiedzialnosci, denorms):
"""Dwie prace w jednej jednostce: Zielińskiego i Abackiego.

Nazwiska dobrane tak, żeby porządek alfabetyczny był ODWROTNY do kolejności
wstawiania — inaczej test przechodziłby także bez sortowania.

``denorms.flush()`` jest konieczny: ``opis_bibliograficzny_autorzy_cache``
to pole denormalizowane, więc bez przeliczenia zostaje puste i sortowanie
po nim nie miałoby czego porównywać.
"""
jednostka = baker.make(Jednostka, skupia_pracownikow=True)
autorzy = []
for nazwisko, imiona in (("Zieliński", "Jan"), ("Abacki", "Adam")):
autor = baker.make(Autor, nazwisko=nazwisko, imiona=imiona)
praca = baker.make(Wydawnictwo_Ciagle, rok=2020, punkty_kbn=10)
praca.dodaj_autora(autor, jednostka, zapisany_jako=nazwisko)
autorzy.append(autor)
denorms.flush()
return jednostka, autorzy


def _widok(definicja, obiekt, request):
v = RaportGenerujView()
v.kwargs = dict(slug=definicja.slug, od_roku=2020, do_roku=2020)
v.object = obiekt
v.request = request
return v


@pytest.mark.django_db
def test_sortowanie_po_autorach_ustawia_order_by_raportu(rf, prace_dwoch_autorow):
jednostka, _autorzy = prace_dwoch_autorow
seed_default_reports()
definicja = DefinicjaRaportu.objects.get(slug="raport-jednostek")

v = _widok(
definicja,
jednostka,
rf.get("/", data={"sortowanie": sortowanie.WG_AUTOROW}),
)
ret = v.get_context_data()

assert ret["report"].order_by == sortowanie.WG_AUTOROW_POLA


@pytest.mark.django_db
def test_bez_parametru_raport_zachowuje_kolejnosc_z_columnorder(
rf, prace_dwoch_autorow
):
# Regresja dla istniejących instalacji: brak parametru nie może nadpisywać
# sortowania zapisanego w tabeli.
jednostka, _autorzy = prace_dwoch_autorow
seed_default_reports()
definicja = DefinicjaRaportu.objects.get(slug="raport-jednostek")

v = _widok(definicja, jednostka, rf.get("/"))
ret = v.get_context_data()

assert not ret["report"].order_by


@pytest.mark.django_db
def test_rekord_sortuje_sie_po_nazwisku_pierwszego_autora(prace_dwoch_autorow):
# Założenie, na którym stoi cała funkcja: PostgreSQL porównuje ArrayField
# element po elemencie, więc ORDER BY po opis_bibliograficzny_autorzy_cache
# daje porządek wg nazwiska PIERWSZEGO autora.
jednostka, _autorzy = prace_dwoch_autorow

posortowane = Rekord.objects.prace_jednostki(jednostka).order_by(
"opis_bibliograficzny_autorzy_cache"
)

nazwiska = [r.opis_bibliograficzny_autorzy_cache[0] for r in posortowane]
assert nazwiska == ["Abacki Adam", "Zieliński Jan"]


@pytest.mark.django_db
def test_formularz_domyslnie_nie_zmienia_sortowania(prace_dwoch_autorow):
jednostka, _autorzy = prace_dwoch_autorow
seed_default_reports()
definicja = DefinicjaRaportu.objects.get(slug="raport-jednostek")

form = form_class_dla(definicja)()

assert form.fields["sortowanie"].initial == sortowanie.DOMYSLNE


@pytest.mark.django_db
def test_formularz_przekazuje_wybor_sortowania_do_url(prace_dwoch_autorow):
# Wybór musi przeżyć skok formularz -> redirect -> widok generujący, bo to
# querystring jest jedynym nośnikiem parametrów raportu (tak samo niesie
# ``_export``, więc eksport do DOCX/XLSX dostaje ten sam porządek).
jednostka, _autorzy = prace_dwoch_autorow
seed_default_reports()
definicja = DefinicjaRaportu.objects.get(slug="raport-jednostek")

form = form_class_dla(definicja)(
data={
"obiekt": jednostka.pk,
"od_roku": 2020,
"do_roku": 2020,
"_export": "html",
"sortowanie": sortowanie.WG_AUTOROW,
}
)
assert form.is_valid(), form.errors

response = _redirect_do_generuj(form.cleaned_data, form.POLA_PRZEKAZYWANE)

assert f"sortowanie={sortowanie.WG_AUTOROW}" in response.url
11 changes: 10 additions & 1 deletion src/nowe_raporty/views.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
from .forms import form_class_dla
from .models import DefinicjaRaportu
from .poziomy import POZIOMY
from .sortowanie import pola_dla


def zastosuj_filtry_zaawansowane(queryset, params):
Expand Down Expand Up @@ -154,6 +155,14 @@ def get_context_data(self, **kwargs):
}
)

# Wariant sortowania z formularza (FD467). Wołamy setter TYLKO dla
# wariantu nadpisującego — dla domyślnego zostawiamy ``ColumnOrder``
# tabeli nietknięty, żeby instalacje, które go sobie przestawiły,
# zachowały swoją kolejność.
pola_sortowania = pola_dla(self.request.GET.get("sortowanie"))
if pola_sortowania:
report.set_order_by(*pola_sortowania)

kwargs["report"] = report
kwargs["od_roku"] = self.kwargs["od_roku"]
kwargs["do_roku"] = self.kwargs["do_roku"]
Expand Down Expand Up @@ -268,7 +277,7 @@ def get_initial(self):

def form_valid(self, form):
return _redirect_do_generuj(
form.cleaned_data, getattr(form, "POLA_ZAAWANSOWANE", [])
form.cleaned_data, getattr(form, "POLA_PRZEKAZYWANE", [])
)

def get_context_data(self, **kwargs):
Expand Down
8 changes: 4 additions & 4 deletions uv.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading