Repository navigation
Commit 14b6cd9
Tabela krzyżowa rekordów i autorów na stronie „Wyszukiwanie zapytaniem" (fazy 2-4) (#686)
* docs(zapytanie): spec wachlarza wyjść dla wyszukiwania zapytaniem
Eksporty (CSV/XLSX/HTML/DOCX/BibTeX) i tabela krzyżowa na stronie
/zapytanie/ w parytecie z multiseekiem, plus nowy rejestr wymiarów
tabeli krzyżowej dla modelu Autor (bazy: kadrowa / prace / udziały).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(zapytanie): self-review speca — 3 poprawki
- Część IV: usunięta sprzeczność (jeden annotate vs osobne zapytania);
metryki dorobku liczone dwoma zapytaniami, bo dwa JOIN-y do relacji
do-wielu w jednym annotate zawyżają sumy
- postać dla model=autor to 'rekordy' + 'pivot' (nie 'lista')
- doprecyzowany routing eksportu przy postac=pivot i sanityzacja ?tytul=
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(zapytanie): plan wdrożenia w 12 zadaniach
Cztery fazy: wspólny silnik wyjścia + eksporty rekordów, pivot rekordów,
pakiet bpp/pivot/ + rejestr autorski (baza kadrowa), metryki
bibliometryczne + eksport autorów.
Self-review planu wykrył trzy blokery reużycia report-body-pivot.html
(formularz GET gubiący model/query, relatywne linki eksportu, selektory
nieświadome baz agregacji) — wszystkie rozpisane jako konkretne edycje.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* docs(plan): konwencje testowe repo + mocniejsze asercje
Pre-flight scan planu: testy dotykające Rekordu wymagają fixture denorms
i denorms.flush() (Rekord to zdenormalizowany cache), fixture 'tytul' nie
istnieje (jest 'tytuly'), a dwie asercje były zbyt słabe (dowolne '2'
w wierszu CSV; brak '5000' w komunikacie) — zamienione na parsowanie CSV
i sprawdzenie treści komunikatu.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* refactor(zapytanie): wykonaj_zapytanie() jako jedno źródło prawdy
* test(zapytanie): test dedupu faktycznie produkuje duplikat wierszy
* refactor(multiseek): document_export_response() jako funkcja
* docs: postac to identyfikatory ReportType, nie polskie etykiety
Task 2 wykrył, że plan brał drugi argument ReportType('list', 'lista') —
etykietę UI — za wartość report_type. postac=tabela nigdy nie trafiłaby
w TABLE_REPORT_TYPES, więc tabela renderowałaby się jako lista, bez błędu.
Poprawione w planie (zadania 2-6, 12) i w specyfikacji.
Druga poprawka: nh3 w sanitize_export_html zdejmuje atrybuty class, więc
asercje testów eksportu HTML/DOCX nie mogą opierać się na klasach CSS —
tylko render na stronie je zachowuje.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(zapytanie): postać wyniku (lista/tabela/punktacja) z partiali multiseeka
* docs(plan): pivot wchodzi do <select> dopiero gdy działa
Review Task 3: postać 'tabela krzyżowa' była wybieralna, a renderowała
placeholder listy (dla autorów: puste wiersze, bo Autor nie ma
js_safe_pk ani opisu bibliograficznego). Zadanie 3 usuwa ją z list
postaci, zadania 6 i 9 dopisują ją z powrotem wraz z implementacją.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(zapytanie): eksport CSV/XLSX/HTML/DOCX/BibTeX z zapytania DjangoQL
* fix(zapytanie): review fixes — usun niedzialajacy pivot, uzyj partiala bibtex, ikona fi-page-edit
- POSTACIE_REKORD/POSTACIE_AUTOR: wyrzucone "pivot" (bez dedykowanego
partiala renderowal sie jako zepsuta lista rekordow/autorow).
- postac="bibtex" renderuje teraz wlasny partial multiseeka
(report-body-bibtex.html) zamiast placeholder-owej listy opisow.
- ikona edycji ✎ -> fi-page-edit (Foundation Icons, zgodnie z konwencja
repo dla frontendu publicznego) w obu partialach (list/table).
- test_postac_tabela_ma_sumy: asercja na realnej wartosci sumy (dwa
rekordy, 40+10=50,00), nie tylko na obecnosci kontenera.
- nowe testy regresyjne: pivot niewybieralny/degradujacy, bibtex uzywa
wlasnego partiala, pager zachowuje postac miedzy stronami, stopka
"Suma:" tylko na ostatniej stronie tabeli.
* fix(zapytanie): chowaj martwy przycisk kopiowania BibTeX poza multiseekiem
Przycisk "Skopiuj wszystko do schowka" w report-body-bibtex.html ma
handler JS tylko inline w multiseekowym common-results.html — na
/zapytanie/ (hide_chrome=True) byl martwy (klik bez efektu). Ten sam
wzorzec co widget usuwania (❌): {% if not hide_chrome %} wokol
.bibtex-actions. Multiseek nie przekazuje hide_chrome, wiec jego
przycisk dziala jak dotad; /zapytanie/ dostaje sam <pre> z BibTeX-em
(w pelni zaznaczalny/kopiowalny recznie).
* fix(zapytanie): eksport — 400 jako text/plain + escape() błędu DjangoQL (XSS)
* feat(zapytanie): pasek eksportu w UI + newsfragment
* docs(plan): zadanie 11 przywraca pasek eksportu dla autorów
Zadanie 5 celowo zwróciło pustą krotkę formatów dla model=autor, bo
backend 400-ował każdy format i pasek pokazywałby martwe linki. Po
wdrożeniu eksportu autorów gałąź ma wrócić do (csv, xlsx).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* fix(zapytanie): poprawki z końcowego review — asercja 403, martwe re-eksporty, komunikat nieznanego modelu
Test uprawnien testuje teraz sztywno 403 (raise_exception=True gwarantuje
to niezaleznie od stanu logowania — potwierdzone w kodzie mixinow Django).
Usuniete szesc martwych re-eksportow z mymultiseek.py (niepotwierdzone
grepem uzycie poza tym plikiem). Rozdzielony komunikat 400 dla eksportu
autorow i dla realnie nieznanego modelu. Dwa testy degradacji postaci dla
autora dostaly pozytywna asercje na "rekord-id-cell" zamiast samego
"not in".
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
* feat(zapytanie): tabela krzyżowa rekordów na stronie zapytania
Postać "tabela krzyżowa" (postac=pivot) wraca do selektora dla modelu
rekord — zadanie 3 ją usunęło, bo bez implementacji renderowała placeholder
listy. Silnik pivota (parse_pivot_params/zbuduj_pivot/DIMENSIONS/METRICS)
jest brany z multiseeka bez zmian; widok dokłada tylko kontekst i trzy
klucze, których partial potrzebuje pod tym URL-em: form action, ukryte pola
model/query/postac (strona zapytania trzyma filtr w URL-u, nie w sesji jak
multiseek) i prefiks linków eksportu. Eksport macierzy do CSV/XLSX omija
capy rekordowe 25000/5000 — rozmiar macierzy nie zależy od liczby rekordów
źródłowych. Bramka PIVOT_MAX_CELLS pokazuje komunikat na ekranie zamiast 500.
Przy okazji naprawione cztery fałszywie zielone asercje testowe: sprawdzały
obecność gołej nazwy klasy "rekord-id-cell", która występuje na KAŻDEJ
stronie, bo zapytanie.html definiuje dla niej regułę CSS w bloku <style>.
Test "pivot dla rekordu degraduje" przechodził więc nadal, mimo że pivot
dostał już własny render. Asercje celują teraz w '<td class="rekord-id-cell">',
a strażnik wybieralności pivota został odwrócony na model autor, który
własnego rejestru wymiarów jeszcze nie ma.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* refactor(pivot): silnik do bpp/pivot/, multiseek_registry re-eksportuje
Silnik tabeli krzyżowej przestał być multiseek-specyficzny w momencie, gdy
dostała ją też strona „Wyszukiwanie zapytaniem", a zadania 8-10 dokładają
drugi rejestr wymiarów (dla modelu Autor, z trzema bazami agregacji). Kod
rozdzielony na generyczny silnik (bpp/pivot/core.py) i rejestr wymiarów
Rekordu (bpp/pivot/rekord.py); multiseek_registry/pivot.py zostaje cienkim
re-eksportem, żeby nie ruszać call-site'ów w szablonach i widokach.
Jedyna zmiana sygnatury: zbuduj_pivot(..., *, model=None) — strategia dedupu
potrzebuje modelu bazowego, dotąd zaszytego jako Rekord. Domyślny None
zachowuje wszystkie istniejące wywołania; zadanie 8 poda tu Autora.
Progi PIVOT_MAX_CELLS/PIVOT_MAX_PAIRS świadomie NIE są re-eksportowane przez
shim: zbuduj_pivot czyta je jako globalne swojego modułu, więc kopia nazwy
pod starą ścieżką byłaby martwa — monkeypatch na niej przestawiłby kopię, nie
bramkę. Trzy testy patchujące progi celują teraz w bpp.pivot.core, gdzie te
stałe mieszkają. Brak nazwy w shimie jest tu cechą, nie luką: monkeypatch
domyślnie podnosi AttributeError na nieistniejącym atrybucie, więc pomyłka
wyjdzie głośno w linii patcha, zamiast dać fałszywie zielony test.
Neutralność: 25 testów pivota multiseeka przechodzi bez zmian w asercjach
(zmienione tylko trzy cele monkeypatcha — patch site idzie za przeprowadzką
stałej dokładnie tak jak import site). Cała suita pivot/zapytanie/multiseek:
458 passed.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(pivot): rejestr autorski, baza kadrowa (liczba autorów)
Drugi rejestr silnika pivota (bpp/pivot/autor.py) obok rejestru rekordowego
z zadania 7 — model Autor, na razie tylko baza K (kadrowa, metryka „liczba
autorów"). Bazy P (liczba prac) i U (Σ slotów, Σ pkdaut) to zadanie 10; seam
dla nich jest gotowy (PivotDimension.expr jako dict per baza, PivotMetric.baza),
więc dołożenie ich nie wymaga przeprojektowania.
core.py: PivotDimension zyskuje annotation (adnotacja ORM dokładana przed
grupowaniem — potrzebna dla wymiarów bool typu „ma ORCID", gdzie grupowanie po
surowym polu dałoby grupę na wartość) oraz expr_dla()/alias() do odczytu
ścieżki per baza; PivotMetric zyskuje baza i distinct_field. Rejestr rekordowy
nie zauważa zmiany — przekazuje expr jako zwykły str; 25 testów bez zmian.
Poprawki względem brief-u: fixture „tytul" w tym repo nie istnieje (jest tylko
„tytuly") — Tytul tworzony inline; test odrzucania wymiaru niedostępnego w
bazie był wsobny przy jednej bazie K (klucz „rok" nie istnieje w DIMENSIONS
autorskich, więc fallback zadziałałby i bez logiki expr_dla) — przemianowany na
to, co faktycznie sprawdza; asercja liczby autorów zastąpiona pełnym mapowaniem
etykieta→suma ze stałą BRAK zamiast wklejonego literału.
Nośność dedupu zmierzona, nie założona: po usunięciu dedupu w
zbuduj_pivot_autora wszystkie 7 testów nadal przechodzi, bo Count(pk,
distinct=True) sam zwija wiersze zwielokrotnione JOIN-em. Dedup zostaje
(poprawny, tani, jednakowy dla baz), ale test przestał twierdzić, że go
dowodzi, a wymóg testu padającego bez dedupu — możliwego dopiero dla metryk Σ,
gdzie Sum nie rozpozna duplikatu — jest dopisany do zadania 10 w planie.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(zapytanie): tabela krzyżowa autorów w UI + presety
Rejestr pivota autorskiego (bpp.pivot.autor, baza kadrowa K) zostaje
wpięty w /zapytanie/ i eksport: postac=pivot jest teraz wybieralna dla
obu modeli, ZapytanieView._pivot_context i ZapytanieExportView
rozgałęziają się po modelu przez wspólny helper
bpp.pivot.wybierz_rejestr_pivota, eksport macierzy autorskiej (CSV/XLSX)
naprawdę działa i ma link w pasku, a sekcja pomocy dostaje cztery
klikalne presety (struktura kadrowa, ORCID, PBN UID, płeć wg tytułów).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(pivot): metryki bibliometryczne autorów (liczba prac, Σ slotów, Σ pkdaut)
Bazy P (prace, przez `autorzy` → Rekord) i U (udziały, przez
`cache_punktacja_autora_query` → Rekord) w rejestrze pivota autorskiego,
razem z wymiarami publikacyjnymi o `expr` per baza: rok, dyscyplina,
jednostka przy pracy, typ odpowiedzialności, charakter formalny.
KRYTYCZNE: każdy wymiar publikacyjny idzie DOKŁADNIE tą samą relacją
do-wielu, po której agreguje metryka jego bazy (P → `autorzy__…`,
U → `cache_punktacja_autora_query__…`). Zmieszanie dwóch relacji daje
iloczyn kartezjański i `Sum` zwraca wielokrotność prawdy — zmierzone:
autor z dwiema pracami i dwoma wierszami udziału daje Σ slotów 3.0
zamiast 1.5. Pilnuje tego test niezawyżania na dokładnej liczbie.
Dedup przez świeży queryset w `zbuduj_pivot_autora`, dotąd redundantny
w bazie K (`Count(pk, distinct=True)` sam zwija duplikaty), staje się
nośny dla metryk Σ — dowodzi tego nowy test, który po zakomentowaniu
tamtej linii pada (0.5 → 1.5).
Do UI dochodzą cztery presety oparte na nowych bazach (produktywność
jednostek, ranking slotowy autorów, udziały dyscyplinowe, wkład
punktowy jednostek) plus test, że każdy preset przeżywa
`parse_pivot_params_autor` bez cichej podmiany wymiaru.
Zdjęty `xfail(strict=True)` z `test_metryka_slotowa_pokazuje_rok`.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* feat(zapytanie): eksport autorów CSV/XLSX z metrykami dorobku
ZapytanieExportView eksportuje teraz listę autorów (nie tylko macierz
pivota) do CSV/XLSX: dane kadrowe (jednostka, tytuł, ORCID, PBN UID i
inne) plus liczba prac, Σ slotów i Σ pkdaut. _metryki_dorobku() liczy
te trzy metryki DWOMA osobnymi zapytaniami (Count przez relację
"autorzy", Sum przez "cache_punktacja_autora_query") — jeden wspólny
annotate() na dwóch różnych relacjach do-wielu zawyżałby sumy (dowód:
test_eksport_autorow_nie_zawyza_metryk). Te same ścieżki ORM, co
metryki bpp.pivot.autor.METRICS, żeby lista i tabela krzyżowa autorów
nigdy nie pokazały dwóch różnych liczb dla tej samej populacji.
eksport_formaty() (zapytanie.py) pokazuje teraz pasek CSV/XLSX dla
modelu "autor" niezależnie od postaci wyniku — wcześniej ograniczone
do postac="pivot", bo lista 400-owała.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* test(zapytanie): test Playwright postaci wyniku + dokumentacja
Dopisuje test przegladarkowy dowodzacy, ze wybor postaci wyniku
(#id_postac) faktycznie jedzie przez formularz GET /zapytanie/ - domyslna
tabela redakcyjna ustepuje partialowi multiseeka po przelaczeniu na
"lista", a URL zawiera postac=list. Dodaje sekcje w dokumentacji redaktora
o postaciach wyniku, eksporcie i tabeli krzyzowej strony /zapytanie/.
Newsfragmenty na ta funkcjonalnosc juz istnieja z wczesniejszych zadan
tego planu (zapytanie-eksporty, zapytanie-tabela-krzyzowa[-autorzy],
zapytanie-pivot-autor-metryki-bibliometryczne, zapytanie-eksport-autorow)
- nie dodano duplikatu.
Pelna suita bez Playwrighta: 8748 passed, 4 skipped, 1 xfailed.
Suita Playwright: 119 passed. pre-commit: czysto.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
* fix(zapytanie): poprawki z końcowego review faz 2-4
K1 (krytyczne) — górny pasek eksportu przy postac=pivot ściągał INNĄ macierz
niż widoczna: budował URL z samych model/query/postac, więc gubił
pivot_row/pivot_col/pivot_val. Zmierzone: link z górnego paska zwracał
"Rok,RAZEM / 2026,1,1 / RAZEM,1" (macierz domyślna), a link spod macierzy
"Charakter ogólny (rodzaj),…" — dwa przyciski CSV kilka centymetrów od
siebie, dwa różne pliki. Górny pasek nie renderuje się teraz przy pivocie:
pasek w report-body-pivot.html składa URL z request.GET.urlencode, więc jest
z konstrukcji zgodny z ekranem (także po dołożeniu kolejnego parametru
pivota) i stoi przy selektorach, które macierz konfigurują. Wariant
"dołóż trzy parametry do href-a" odrzucony — zostawiałby dwie listy
parametrów do ręcznego synchronizowania, czyli dokładnie mechanizm, który
K1 wyprodukował.
W1 — pivot autorski nigdy nie pokazywał adnotacji o dublowaniu (twarde
has_autorzy_dim=False), choć baza P dubluje tak samo jak pivot rekordowy.
Zmierzone: jedna praca, dwóch autorów z różnych jednostek, row=jednostka →
GRAND 2 przy jednej unikatowej pracy i BRAK ostrzeżenia (a to jest preset
"Produktywność jednostek"). Flaga liczona teraz uczciwie: nowe pole
PivotDimension.atrybut_rekordu (ten sam kształt co `autorzy` w rejestrze
rekordowym), ustawione dla `rok` i `charakter_formalny`, plus _dubluje():
baza P + jakikolwiek wymiar, który nie jest atrybutem pracy. Po poprawce
jednostka → 2 + True, rok → 1 + False. Tekst notki przeredagowany na
neutralny modelowo.
W2 — distinct=True w METRICS["liczba_prac"] nie było pokryte: wszystkie
testy grupowały po `autor`, gdzie DISTINCT jest no-opem (zmierzone: bez=1,
z=1; przy row=jednostka bez=2, z=1). Dopisany test dwóch współautorów jednej
jednostki (grand_total == 1) plus testy dublowania/niedublowania z W1.
Mutacja "usuń distinct=True" wywala teraz 2 testy.
W3 — testy eksportu macierzy asertowały samo b"RAZEM", które jest w KAŻDEJ
macierzy (także w błędnej domyślnej) — dlatego K1 przeszedł trzy rundy
review. Cztery testy sprawdzają teraz etykiety zamówionych wymiarów i
dokładne wartości komórek (XLSX przez openpyxl, nie przez Content-Type),
doszedł też test biorący WSZYSTKIE linki eksportu ze strony pivota i
wymagający od każdego macierzy zgodnej z ekranem. Mutacja "eksport ignoruje
parametry GET" wywala 4 testy, mutacja "przywróć stary górny pasek" — 2.
W4 — presety były martwe na świeżej stronie: renderowały się z query= i po
kliknięciu dawały 200 bez wyników, bez macierzy i bez komunikatu. Puste
zapytanie nie jest "wszyscy autorzy" — apply_search rzuca DjangoQLParserError
("Unexpected end of input"), więc widok w ogóle nie wchodzi w render_results.
Bez wysłanego zapytania blok pokazuje więc nazwy presetów jako tekst plus
zdanie, co zrobić, żeby ożyły (discoverability bez klikalnej obietnicy);
z zapytaniem — linki jak dotąd, z tekstem mówiącym wprost, że jadą
z WYSŁANYM zapytaniem, nie z niewysłaną treścią pola.
Drobne: prawdziwy docstring PivotDimension.expr_dla (D2); martwa bramka
{% if eksport_formaty %} i nieaktualny komentarz zastąpione realną bramką
postac != "pivot" (D3); jeden słownik TAK/NIE/— brak — dla wartości
logicznych (core.etykieta_bool) używany i przez pivot, i przez eksport
autorów, gdzie orcid_w_pbn jechał dotąd jako "True"/"False" (D5).
Świadomie odłożone (bez zmian): grupy o sumie 0 znikające z macierzy,
paginacja/URL-e admina liczone na darmo przy postac=pivot, rsplit("csv/").
Weryfikacja: pytest -k "pivot or zapytanie or multiseek" → 496 passed
(było 490), pre-commit czysty, zero zmian w migracjach i baseline-sql.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>1 parent f6e935d commit 14b6cd9
33 files changed
Lines changed: 7312 additions & 482 deletions
File tree
- docs
- redaktor
- superpowers
- plans
- specs
- src
- bpp
- multiseek_registry
- newsfragments
- pivot
- templates/bpp
- tests
- test_playwright
- views
- django_bpp/templates/multiseek
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
238 | 238 | | |
239 | 239 | | |
240 | 240 | | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
0 commit comments