From 7eb393f497742506fb618783454eb914e81551f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Mon, 31 Aug 2026 10:49:06 +0200 Subject: [PATCH 1/2] =?UTF-8?q?feat(pivot):=20stronicowanie=20i=20sortowan?= =?UTF-8?q?ie=20tabeli=20krzy=C5=BCowej?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Tabela krzyżowa renderowała wszystkie wiersze macierzy naraz — przy pivot_row=autor to ~2000 wierszy na jednej stronie. Nie było też żadnego sterowania kolejnością: jedyną dostępną było alfabetyczne _labels(). Sortowanie i stronicowanie działają na JUŻ ZBUDOWANEJ macierzy, nie na SQL-u: row_totals powstają zanim _labels() posortuje wiersze, więc sortowanie po sumie nie kosztuje dodatkowego zapytania. Wariant SQL-owy (LIMIT/OFFSET na kluczach wierszy) wymagałby drugiego przebiegu po sumy brzegowe i płacił drogi JOIN do autorzy__ dwa razy. Nowy PivotWidok (sort/kierunek/strona/na_stronie) + parse_widok() żyją w bpp.pivot.core, a nie w rejestrach — dzięki temu sygnatura parse_params() i jej pięć miejsc wywołania zostają nietknięte. as_table() wołane bez argumentu zachowuje się dokładnie jak dotąd. Szczegóły: - rozmiar strony walidowany względem whitelisty (25/50/100/250); bez tego ?pivot_per_page=999999 unieważnia całą zmianę, - sortowanie po sumie rozstrzyga remisy etykietą — bez porządku totalnego wiersz o równej sumie mógłby trafić na dwie strony naraz albo zniknąć z obu, - col_totals/grand_total NIE są przeliczane per strona (semantyka Excela); pod tabelą staje o tym adnotacja, - linki sortujące i pager kasują print=None, bo {% querystring %} przenosi cały GET, a common-results.html odpala window.print() dla ?print=1, - ukryte pola pivot_sort/pivot_dir w formularzu kontrolek: selecty auto-submitują, więc bez nich zmiana wymiaru gubiłaby sortowanie. pivot_page celowo NIE jest przenoszone — zmiana wymiaru wraca na 1, - eksport XLSX/CSV respektuje sortowanie, ignoruje stronę (pełna macierz w pliku) — dlatego UI nie ma opcji „pokaż wszystkie", - PIVOT_MAX_CELLS 10 000 -> 50 000: po stronicowaniu render przestał być wąskim gardłem. PIVOT_MAX_PAIRS bez zmian (bezpiecznik pamięci). Działa identycznie w obu wejściach — multiseek „precyzyjne" i /zapytanie/ (DjangoQL) dzielą partial multiseek/report-body-pivot.html. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_0132gLD38SwHVWMY1kemmkQt --- ...1-pivot-stronicowanie-sortowanie-design.md | 153 +++++++++ src/bpp/multiseek_registry/pivot.py | 3 + ...pivot-stronicowanie-sortowanie.feature.rst | 6 + src/bpp/pivot/__init__.py | 3 + src/bpp/pivot/core.py | 192 ++++++++++- src/bpp/static/scss/_multiseek-reports.scss | 38 +++ src/bpp/templates/bpp/zapytanie.html | 5 +- src/bpp/tests/test_multiseek_pivot_view.py | 90 +++++ src/bpp/tests/test_pivot_widok.py | 307 ++++++++++++++++++ src/bpp/tests/test_zapytanie_pivot.py | 140 ++++++++ src/bpp/views/multiseek_export.py | 18 +- src/bpp/views/mymultiseek.py | 15 +- src/bpp/views/zapytanie.py | 6 + src/bpp/views/zapytanie_export.py | 7 +- .../templates/multiseek/_pivot-pager.html | 58 ++++ .../multiseek/report-body-pivot.html | 50 ++- 16 files changed, 1068 insertions(+), 23 deletions(-) create mode 100644 docs/superpowers/specs/2026-08-31-pivot-stronicowanie-sortowanie-design.md create mode 100644 src/bpp/newsfragments/pivot-stronicowanie-sortowanie.feature.rst create mode 100644 src/bpp/tests/test_pivot_widok.py create mode 100644 src/django_bpp/templates/multiseek/_pivot-pager.html diff --git a/docs/superpowers/specs/2026-08-31-pivot-stronicowanie-sortowanie-design.md b/docs/superpowers/specs/2026-08-31-pivot-stronicowanie-sortowanie-design.md new file mode 100644 index 000000000..c5f90c8c3 --- /dev/null +++ b/docs/superpowers/specs/2026-08-31-pivot-stronicowanie-sortowanie-design.md @@ -0,0 +1,153 @@ +# Tabela krzyżowa: stronicowanie i sortowanie + +Data: 2026-08-31 +Branch: `feat-pivot-stronicowanie-sortowanie` + +## Problem + +Tabela krzyżowa (`postac=pivot` na `/zapytanie/`, `report_type=pivot` +w multiseeku) renderuje **wszystkie** wiersze macierzy naraz. Przy +`pivot_row=autor` to ~2000 wierszy na jednej stronie — nieużywalne. + +Brakuje też jakiegokolwiek sterowania kolejnością: `_labels()` sortuje +alfabetycznie po etykiecie (albo malejąco po roku dla `rok`/`koszyk_pk`) +i to jedyna dostępna kolejność. Typowe pytanie użytkownika brzmi „kto ma +najwięcej prac" — dziś wymaga eksportu do XLSX i posortowania w Excelu. + +## Zakres + +1. Stronicowanie wierszy macierzy — **obie** ścieżki wejścia (multiseek + „precyzyjne" i `/zapytanie/` DjangoQL), bo obie renderują ten sam + partial `multiseek/report-body-pivot.html`. +2. Sortowanie wierszy: po etykiecie (dzisiejsze, domyślne) albo po sumie + wiersza (RAZEM), oba kierunki. + +Poza zakresem: sortowanie kolumn (kolumn jest z definicji mało — bramka +`PIVOT_MAX_CELLS` i tak by nie przepuściła szerokiej macierzy), sortowanie +po konkretnej kolumnie, stronicowanie kolumn. + +## Rozważone podejścia + +| | Opis | Werdykt | +|---|---|---| +| A | Sortuj + stronicuj **już zbudowaną** macierz w pamięci | **wybrane** | +| B | `LIMIT/OFFSET` na kluczach wierszy w SQL-u | odrzucone | +| C | Sortowanie/stronicowanie po stronie klienta (DataTables) | odrzucone | + +**B** wymagałoby drugiego przebiegu po zbiorze, żeby policzyć sumy kolumn +i sumę całkowitą (te muszą obejmować cały dataset, nie widoczną stronę), +a przy wymiarze idącym przez `autorzy__` płaciłoby ten sam drogi JOIN +dwa razy. Bramka `PIVOT_MAX_PAIRS` i tak ogranicza rozmiar tego, co wchodzi +do RAM-u, więc oszczędność pamięci byłaby iluzoryczna. + +**C** przeczy sednu zgłoszenia: 2000 wierszy nadal poleciałoby do +przeglądarki. Strona musi też działać bez JS (partial ma `