Skip to content

fix(tests): deterministyczny wybór w Select2 helperze (klik zamiast Enter)#274

Merged
mpasternak merged 1 commit into
devfrom
fix/select2-helper-exact-match
Jun 1, 2026
Merged

fix(tests): deterministyczny wybór w Select2 helperze (klik zamiast Enter)#274
mpasternak merged 1 commit into
devfrom
fix/select2-helper-exact-match

Conversation

@mpasternak

Copy link
Copy Markdown
Member

Problem

Flake na CI (PR #189, shard 5/8): test_procent_odpowiedzialnosci_baseModel_AutorFormset_dwoch_autorow[chromium-wydawnictwo_zwarte] wywalał się Playwright TimeoutError na wait_for_function. To był jednak tylko wtórny objaw — na serwerze leciał HTTP 500:

psycopg2.errors.UniqueViolation: ... "bpp_wydawnictwo_zwarte_a_rekord_id_autor_id_typ_o_efb01db8_uniq"
DETAIL: Klucz (rekord_id, autor_id, typ_odpowiedzialnosci_id)=(37, 38, 15) już istnieje.

Oba wiersze formsetu autorów wylądowały z tym samym autor_id, choć test wybierał dwóch różnych autorów. Strona nigdy nie pokazała „dodany pomyślnie" → timeout Playwrighta.

Root cause

select_select2_autocomplete (src/django_bpp/playwright_util.py) wciskał Enter, wybierając PIERWSZĄ podświetloną pozycję wyników — bez weryfikacji, że to faktycznie szukany rekord.

Testowi autorzy dzielą prefiks nazwiska: Aut1<suffix> / Aut2<suffix>. Pod obciążeniem CI press_sequentially wpisuje znaki wolniej niż debounce Select2 (~250 ms), więc dla zapytania "Aut2…" w oknie czasowym widoczne bywają jeszcze wyniki dla "Aut…" — zawierające autora pierwszego. Warunek oczekiwania (!isLoading && hasResults) spełniają te nieświeże, szersze wyniki, a Enter wybiera złego autora. Oba wiersze → ten sam autor_idUniqueViolation przy zapisie.

To nie jest regresja PR #189 — jedyna zmiana tego PR-a w admin/core.py to klucz cache w filter_count_view (inna ścieżka niż save_related). Pre-existing timing-flake Select2. Dodatkowo: w pytest.ini jest --only-rerun TimeoutError, ale brak --reruns N → realnie 0 retry, więc jeden błędny strzał wywala cały shard.

Fix

Helper klika teraz konkretną pozycję, której widoczny tekst zawiera pełną szukaną wartość, zamiast wciskać Enter na pierwszej podświetlonej. Tylko gdy takiej pozycji nie ma (pola, gdzie szukany tekst nie jest podłańcuchem etykiety — np. generowane warianty „zapisany jako" typu Kopara1) helper spada do dawnego Enter-na-pierwszej.

Naprawia całą klasę tych flake'ów, nie tylko ten jeden test.

Weryfikacja

Lokalnie, na zbudowanych assetach + testcontainers:

test_..._dwoch_autorow × {wydawnictwo_ciagle, wydawnictwo_zwarte, patent} × 3 powtórki = 9/9 passed

Happy-path bez regresji; ścieżka fallback (Enter dla zapisany_jako) zweryfikowana, bo Kopara1/2 nie są podłańcuchem etykiety. Timing-flake reprodukuje się głównie pod obciążeniem CI — ostateczna walidacja na zielonym CI tego PR-a.

🤖 Generated with Claude Code

…j pozycji zamiast Enter

select_select2_autocomplete wciskał Enter wybierając PIERWSZĄ podświetloną
pozycję wyników. Pod obciążeniem CI press_sequentially wpisuje znaki wolniej
niż debounce Select2, więc dla "Aut2…" w oknie czasowym widoczne bywają jeszcze
wyniki dla "Aut…" — zawierające INNEGO autora o wspólnym prefiksie. Enter
po cichu wybierał złego autora; błąd ujawniał się dopiero przy zapisie jako
UniqueViolation na (rekord_id, autor_id, typ_odpowiedzialnosci_id), bo oba
wiersze formsetu wskazywały na ten sam autor_id.

Helper klika teraz konkretną pozycję, której widoczny tekst zawiera pełną
szukaną wartość; tylko gdy takiej pozycji nie ma (pola, gdzie szukany tekst
nie jest podłańcuchem etykiety, np. generowane warianty "zapisany jako")
spada do dawnego Enter-na-pierwszej.

Weryfikacja lokalna: test_..._dwoch_autorow × 3 parametry × 3 powtórki = 9/9
passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@mpasternak mpasternak merged commit 09f996b into dev Jun 1, 2026
9 checks passed
@mpasternak mpasternak deleted the fix/select2-helper-exact-match branch June 1, 2026 11:11
mpasternak added a commit that referenced this pull request Jun 2, 2026
…Faza 1) (#164)

* fix(migrations): merge leaf nodes 0005 w importer_publikacji

Gałąź dev po zmerge-owaniu PR #115 (fix/fd-316) i innych ma dwa
równoległe "0005_*" nody w aplikacji importer_publikacji:
- 0005_alter_importsession_created_by
- 0005_importsession_wydawnictwo_nadrzedne

Oba dziedziczą po 0004_rename_user_to_created_by_add_modified_by i
robią niezależne operacje (alter FK vs add dwa nowe FK). Konflikt
blokował test suite (makemigrations --check failuje). Migracja
merge-owa 0006_merge_* łączy obie linie bez zmiany istniejących
plików migracji.

* feat(pbn_api): narzędzie pbn_test_wysylka_interaktywna do audytu PBN

Dodano interaktywny Django management command pozwalający krok po kroku
przetestować pełen flow wysyłki publikacji i oświadczeń do PBN. Dla
każdego żądania HTTP pokazuje metodę, URL i body; dla odpowiedzi —
status i treść (skrócone, z opcją pełnego wyświetlenia).

Narzędzie nie modyfikuje lokalnej bazy BPP — używa wyłącznie niższych
metod klienta PBN (transport.post/delete/get_pages) oraz istniejących
helperów (delete_all_publication_statements, post_discipline_statements).
Posiada tryb --dry-run (pokazuje bez wysyłania) oraz --yes-all (auto-
-akceptacja Enter / yes-no z defaultem).

Flow: wybór publikacji → generowanie JSON (z oznaczeniem czy zawiera
statements) → wybór endpointa /api/v1/publications albo repozytoryjnego
/api/v1/repositorium/publications (z wymuszeniem usunięcia klucza
"statements" zgodnie ze spec PBN) → POST publikacji → GET aktualnych
oświadczeń w PBN → porównanie z lokalnymi → (opcjonalnie) DELETE +
POST /v2/institution-profile/statements. Po każdym błędzie HTTP
wypisuje czytelny komunikat i wraca do menu/podsumowania bez crash-a.

Służy jako faza 1 docelowej refaktoryzacji sync_publication — baza
empiryczna do zbadania jak PBN reaguje na poszczególne kombinacje
endpointów zanim zmienimy kolejność operacji DELETE→POST→DOWNLOAD
w src/pbn_api/client/publication_sync.py (problem: nieudana wysyłka
publikacji powodowała utratę oświadczeń w profilu instytucji).

Plan kompletnej poprawki: docs/pbn-wysylka-plan.md.
Flaga .docker-build: żeby CI zbudował obraz do testów ręcznych.

13 testów jednostkowych pokrywa: walidację argumentów, dry-run, happy
path dla obu endpointów, obsługę błędów HTTP, quit na wybór endpointa,
różnice oświadczeń i decyzje użytkownika (zgoda / odmowa DELETE+POST).

* ci(docker): buduj obrazy tylko z pull_request (nie z push do feature/**)

Do tej pory workflow build-docker-images.yml miał trigger na push do
master + feature/** + fix/** + hotfix/** ORAZ na pull_request events.
W rezultacie dla każdego commita w branchu z otwartym PR-em odpalały
się dwa niezależne buildy (push event i pull_request synchronize),
każdy po ~6 minut na Docker Cloud Build — marnowanie minut.

Concurrency group nie grupował tych runów w jedno, bo github.ref jest
różny dla push ("refs/heads/feature/...") i pull_request
("refs/pull/XXX/merge").

Zmiana: push trigger tylko dla master (release flow). Branch feature/**,
fix/**, hotfix/** buduje się wyłącznie przez pull_request events
(opened/synchronize/reopened/labeled), z .docker-build w drzewie lub
labelem docker-build na PR — tak jak wcześniej. Ad-hoc build branchy
bez PR-a pozostaje dostępny przez workflow_dispatch
(`gh workflow run build-docker-images.yml --ref <branch>`).

Efekt: jeden build per commit w PR zamiast dwóch.

* docs(pbn): rozszerzona instrukcja testowania pbn_test_wysylka_interaktywna

Dopisano do docs/pbn-wysylka-plan.md sekcję "Jak testować narzędzie —
krok po kroku" z konkretnymi instrukcjami uruchomienia:

1. Testy jednostkowe (bez PBN)
2. Smoke test na preprod PBN w trybie --dry-run
3. Rzeczywisty test na preprod PBN (z --user-token lub zaciągniętym
   z Uczelnia.pbn_api_user)
4. Uruchomienie w obrazie Docker zbudowanym przez CI
5. Opis każdego z 8 kroków flow (co robi, co pyta, co pokazuje)
6. Checklist obserwacji do zebrania w preprod (dla Fazy 2)
7. Rozwiązywanie typowych problemów (brak tokena, brak uczelni,
   DaneLokalneWymagajaAktualizacjiException, HTTP 423, konflikt
   migracji)

Poprzednia sekcja "CLI argumenty" miała błędny argument `--user` —
zastąpiona prawdziwym `--user-token` (z PBNBaseCommand) plus opcją
automatycznego zaciągania tokena z Uczelnia.pbn_api_user.

Dopisano też notatkę o zmianie w workflow build-docker-images.yml
(one-build-per-commit).

* fix(pbn-test-narzedzie): porównuj intencję BPP (live) zamiast cache i pytaj osobno o DELETE/POST

Dwa buggi zgłoszone przez usera po testowaniu w preprod:

BUG 1 — fałszywa identyczność porównania. Narzędzie porównywało
OswiadczenieInstytucji (lokalny cache PBN) z aktualnym stanem PBN.
Gdy user zmienił coś w rekordzie (skasował autora/dyscyplinę),
cache pozostawał nieaktualny — narzędzie pokazywało 3 identyczne
oświadczenia mimo że BPP by wysłał tylko 2.

Fix: _step_compare_statements używa teraz intencji BPP live —
WydawnictwoPBNAdapter(publication).pbn_get_api_statements() —
zamiast cache'a. Obsługa DaneLokalneWymagajaAktualizacjiException
(zwraca "różnice" ze stosownym komunikatem). KROK 1/8 pokazuje
zarówno cache count jak i intencję live, żeby od razu widać było
ewentualny rozjazd.

BUG 2 — brak kontroli przy identyczności. Gdy porównanie zwróciło
"identyczne", _run_flow robił wczesny return i kończył flow bez
pytania usera. User nie mógł wymusić DELETE+POST żeby sprawdzić
empirycznie reakcję PBN.

Fix: zawsze pytamy osobno o DELETE i osobno o POST. Default zależy
od wyniku porównania (n dla identycznych, t dla różnic), ale pytanie
jest zadane w obu przypadkach. Usunięto redundantne wewnętrzne
prompty ("Wyślij DELETE?", "Wyślij POST?") z _step_delete_statements
i _step_post_statements — zastąpione jednym pytaniem wyższego
poziomu w _run_flow.

Testy:
- Zaktualizowano listy inputów w _patch_input dla nowego flow
  (dwa dodatkowe pytania DELETE/POST zawsze zadawane).
- Dodano helper _patch_intended_statements dla mockowania adaptera
  w testach (fixture nie tworzy PublikacjaInstytucji_V2).
- Dodano regression test test_compare_uses_intended_not_cache_bug1
  pilnujący że cache OswiadczenieInstytucji nie jest już używany
  w porównaniu (weryfikacja przez asercję na output KROK 6/8).

Wszystkie 14 testów przechodzi lokalnie.

* fix(tests): naprawa 2 pre-existing failing testów na dev

Oba testy failowały na dev w CI (nie dotyczą zmian z tej gałęzi,
ale blokowały zielony pipeline).

test_sprobuj_wyslac_do_pbn_celery (bpp/tests/test_admin_helpers/
test_pbn_api_cli.py):
- fixture wydawnictwo_ciagle nie miał DOI/WWW, przez co adapter
  WydawnictwoPBNAdapter rzucał DOIorWWWMissing zanim dojdziemy do
  etapu testującego NeedsPBNAuthorisationException → ustawiamy DOI
  na początku testu.
- cli.py sprawdza user.pbn_token is None, ale model BppUser ma
  default="" (nie None) → test musi wymusić admin_user.pbn_token = None
  in-memory przed sprawdzeniem NeedsPBNAuthorisationException.

test_check_pbn_skip_when_client_fails (importer_publikacji/tests/
test_pbn_check.py):
- @patch("importer_publikacji.views._get_pbn_client") nie działał,
  bo _get_pbn_client jest importowany LOKALNIE w ciele funkcji
  (from .providers.pbn import _get_pbn_client), więc nie jest
  atrybutem modułu views. Fix: patchować u źródła
  importer_publikacji.providers.pbn._get_pbn_client — równoważne,
  bo lokalny import w funkcji złapie zpatchowaną wersję.

Żadnej zmiany w kodzie produkcyjnym (auth logic w cli.py zachowana
bez zmian — fix wyłącznie w testach).

* fix(pbn-test-narzedzie): popraw klucze porównania w KROK 6/8

Klucz porównania zwracał różne struktury dla intencji vs PBN:
- intencja (z pbn_get_api_statements) miała tylko personObjectId,
  a disciplineId był usuwany przez _convert_stmt gdy obecne było
  disciplineUuid — output pokazywał klucze typu ('mongoId', '', None)
- PBN GET /page/statements zwraca (personId, area, institutionId) —
  klucze typu ('mongoId', '301', 'inst_uuid')

Ze strony usera wyglądało to tak że wszystkie 3 oświadczenia były
różne (intencja had "" dla area, PBN had "301"), choć były
faktycznie identyczne.

Fix:
- Użyj pbn_get_json_statements() (surowa lista, przed konwersją
  _convert_stmt) zamiast pbn_get_api_statements()["statements"] —
  zachowuje disciplineId (int, numerek MNiSW).
- Klucz porównania: (person-mongoId, discipline-numerek) z mapowaniem:
  - person: PBN.personId ↔ adapter.personObjectId
  - discipline: PBN.area ↔ adapter.disciplineId (oba = numerek MNiSW)
- Pominięto institutionId w kluczu (zawsze taka sama uczelnia dla
  wszystkich statementów per rekord).

Testy: helper _patch_intended_statements teraz patchuje obie metody
(pbn_get_json_statements dla KROK 6/8, pbn_get_api_statements dla
KROK 8/8) i ustawia pbn_wysylaj_bez_oswiadczen=True na __init__
(żeby StatementsMissing nie wywalił KROK 2/8 gdy intencja jest
pusta — artykuł/rozdział bez statements jest walidowany jako błąd,
a w testach chcemy móc symulować każdy stan).

* fix(migrations): 0007_merge — łączy 0006 z branch i 0006 z dev

Po merge origin/dev (pull dev-a z nowszymi commitami) pojawił się
drugi plik 0006_merge — obecna gałąź miała 0006_merge_20260420_2212
(mój merge z 20-04), dev wniósł 0006_merge_20260421_1100 (z dev-a,
commit 71fe245). Django odrzucał start kontenera:

  CommandError: Conflicting migrations detected; multiple leaf nodes
  in the migration graph: (0006_merge_20260420_2212,
  0006_merge_20260421_1100 in importer_publikacji).

Nowa migracja 0007_merge_20260421_1248 łączy oba leaf-y w jeden node.
Nie modyfikuje istniejących plików migracji.

* feat(pbn): StatementsResendFailedException + Uczelnia.pbn_kasuj_dyscypliny_selektywnie

Commit 1/8 refaktoryzacji sync_publication — model + exception class.

- Nowa klasa StatementsResendFailedException (pbn_api/exceptions.py)
- Nowe pole Uczelnia.pbn_kasuj_dyscypliny_selektywnie BooleanField
  default=True
- Usunięto pole Uczelnia.pbn_api_kasuj_przed_wysylka (obsolete)
- Migracja 0414: RemoveField + AddField
- Zaktualizowane referencje: admin/uczelnia.py, setup_wizard/forms+tests,
  pbn_integrator cli, common.py (usunięto delete_statements_before_upload
  z wywołania sync_publication)
- Dodane noqa dla pre-existing ruff issues (C901 sprobuj_wyslac_do_pbn +
  handle pbn_integrator, E402 dla imports po django.setup()). UP031
  (format specifier) naprawiony w common.py przez f-string.

sync_publication nadal ma parametr delete_statements_before_upload
(zostanie zignorowany w kolejnych commitach gdy dodam split flow).

* refactor(pbn): upload_publication zawsze przez endpoint repo + dead code removal

Commit 2/8 refaktoryzacji sync_publication.

Zmiany w publication_sync.py:
- Usunięto ``post_publication()`` — wrapper na stary /api/v1/publications
- Usunięto ``_should_retry_validation_error()`` — retry loop oparty o
  status 400 z /api/v1/publications (endpoint nie będzie używany)
- Usunięto ``_retry_download_publication()`` — wywoływany tylko z ww.
- Usunięto nieużywany import PBN_POST_PUBLICATIONS_URL
- ``_prepare_publication_json``: zawsze wywołuje
  ``convert_js_with_statements_to_no_statements`` (bo endpoint repo
  wymaga konwersji formatu niezależnie od obecności statements). Zwraca
  tylko ``js`` (było: ``(js, bez_oswiadczen)``).
- ``_post_publication_data``: zawsze używa ``post_publication_no_statements``.
  Usunięto parametr ``bez_oswiadczen``.
- ``upload_publication``: zawsze wysyła przez endpoint repo. Usunięto
  retry loop (był tylko dla starego /api/v1/publications). Sygnatura
  zachowana (``max_retries_on_validation_error`` teraz deprecated/ignorowany).
  Return value ``(objectId, ret, js, True)`` — ostatnie pole zawsze True.
- Fix pre-existing bug ``_delete_statements_with_retry``: warunek
  ``if no_tries < 0`` zmieniony na ``<= 0``. Wcześniej dla max_tries=5
  wykonywał się 6 razy (iteracja 0,1,2,3,4,5). Teraz dokładnie 5.

Ten commit jeszcze NIE dodaje split flow dla statements w sync_publication
— to zostanie zrobione w Commit 4. Między Commitem 2 a 4 testy sync
statements są tymczasowo złamane (sync_publication ignoruje
``download_statements_of_publication`` bo bez_oswiadczen zawsze True).

* feat(pbn): helpery split-flow synchronizacji oświadczeń

Commit 3/8 refaktoryzacji sync_publication — dodanie helperów do nowej
logiki synchronizacji oświadczeń (będą użyte w Commit 4).

Dodane w PublicationSyncMixin:

- ``_statement_key_pbn(stmt)`` / ``_statement_key_intended(stmt)`` —
  staticmethods, zwracają klucz porównania ``(person mongoId, discipline
  numerek)`` jako tuple stringów. Mapowanie: PBN GET response używa
  ``personId``+``area``, adapter ``pbn_get_json_statements`` używa
  ``personObjectId``+``disciplineId`` — oba oznaczają to samo.

- ``_diff_statements(pbn, intended)`` — oblicza różnice między PBN a
  intencją BPP. Zwraca ``(only_in_pbn, only_in_intended)`` — sety
  kluczy do DELETE/POST.

- ``_get_pbn_statements_with_retry(objectId, publication_pk)`` — GET
  aktualnych oświadczeń z PBN, retry x3 z exponential backoff 2s/4s/8s.
  Po wyczerpaniu: rollbar.report_exc_info(level="warning") + raise
  ``StatementsResendFailedException``.

- ``_delete_statements_selective(objectId, stmts, publication_pk)`` —
  per-osoba DELETE (``delete_publication_statement(personId, role)``)
  dla każdego oświadczenia do usunięcia. Retry x3 per oświadczenie.

- ``_delete_statements_batch(objectId, publication_pk)`` — delete_all
  z retry. Propaguje ``CannotDeleteStatementsException`` do callera
  (akceptowalne gdy PBN mówi że oświadczeń nie ma).

- ``_post_statements_with_retry(rec, objectId, publication_pk)`` —
  POST ``/api/v2/institution-profile/statements`` z payloadem z
  ``pbn_get_api_statements``. Wymaga lokalnego ``PublikacjaInstytucji_V2``
  (propaguje ``DaneLokalneWymagajaAktualizacjiException``). Retry x3.

- ``_sync_statements_with_pbn(rec, objectId, kasuj_selektywnie, notificator)``
  — orchestrator: GET → diff → selective/batch DELETE → POST. Obsługa
  4 scenariuszy: identyczne (nic), PBN+BPP= (DELETE), PBN=+BPP
  (POST), różnice (DELETE+POST).

- ``_report_statements_failure_and_raise()`` — wspólny helper do raportowania
  błędów retry do Rollbar (level=warning) + raise StatementsResendFailedException.

- ``_STATEMENT_RETRY_DELAYS = (2, 4, 8)`` — stała exponential backoff.

Import ``StatementsResendFailedException`` dodany. Helpery nie są
jeszcze wywoływane przez ``sync_publication`` — to zostanie zrobione
w Commit 4.

* feat(pbn): sync_publication nowy split flow (POST repo + osobna synchronizacja statements)

Commit 4/8 refaktoryzacji sync_publication. Główna zmiana logiki.

Flow po zmianie:
1. POST publikacji przez ``/api/v1/repositorium/publications`` (zawsze)
2. download_publication — pobierz Publication lokalnie
3. Aktualizacja SentData.pbn_uid
4. Obsługa zmiany/konfliktu PBN UID (_handle_uid_change/_conflict)
5. download_statements_of_publication + pobierz_publikacje_instytucji_v2
   (konieczne bo _post_statements_with_retry wymaga V2 lokalnie)
6. _sync_statements_with_pbn — GET aktualnych w PBN, diff z intencją
   BPP, selektywne/batch DELETE + POST brakujących. Tryb sterowany
   ``Uczelnia.pbn_kasuj_dyscypliny_selektywnie``.

Zmiany:
- Usunięto pre-upload DELETE (``delete_statements_before_upload``
  parametr ignorowany, pozostaje w sygnaturze deprecated dla backward compat)
- Usunięto specjalny notificator dla "bez oświadczeń" (teraz nie rozróżniamy)
- Dodano obsługę ``DaneLokalneWymagajaAktualizacjiException`` przy
  synchronizacji statements — log warning + kontynuacja (publikacja
  została już wysłana w KROK 1, brak V2 lokalnie to nie fatal).

Ważna semantyka błędów:
- POST publikacji fail → wyjątek propagowany; statements w PBN nietknięte
- POST OK + retry statements wyczerpany → ``StatementsResendFailedException``
  (klasyfikowany jako RETRY_LATER w pbn_export_queue — do zrobienia w Commit 7)

* refactor(pbn_integrator): usuń delete_statements_before_upload

Commit 5/8 refaktoryzacji sync_publication — usunięcie parametru
``delete_statements_before_upload`` z callerów ``sync_publication``
(common.py został już zrobiony w Commit 1).

Zmiany:
- ``_synchronizuj_pojedyncza_publikacje`` — usunięty parametr i
  przekazywanie do ``client.sync_publication()``
- ``synchronizuj_publikacje`` — jw., usunięte 3 wywołania wewnętrzne
- ``pbn_integrator`` CLI — usunięty argparse ``--delete-statements-before-upload``
  i handling w ``handle()``

Parametr ``delete_statements_before_upload`` pozostaje w sygnaturze
``sync_publication`` jako deprecated (żeby nie złamać żadnego callera
którego mogłem przeoczyć).

* test(pbn): zaktualizuj testy sync_publication dla nowego split-flow

Commit 6/8 refaktoryzacji sync_publication.

Pliki zmodyfikowane:
- test_client_sync.py — przepisany całkowicie, 17 testów (było 7):
  * 5 happy paths (to same id, tekstowo podane, nowe id, z 0 PK,
    bez istniejacej Publication lokalnie)
  * 1 test że upload zawsze idzie przez endpoint repo
  * 5 scenariuszy sync statements (identyczne, PBN puste+BPP, PBN+BPP
    puste selektywnie/batch, różnice selektywnie)
  * 4 error paths (GET/DELETE/POST retry + POST publikacji fail)
  * 2 unit tests _diff_statements
- test_client_upload.py — URL zmieniony na endpoint repo, SentData
  tworzony z konwertowanym JSON (bo upload zawsze konwertuje teraz)
- test_client_helpers.py — URL repo + pbn_wysylaj_bez_oswiadczen=True
  na uczelnia (test bada afiliację, nie sync statements; monkeypatch
  pbn_get_json_statements=[] żeby nie odpalać POST /v2)
- test_bpp_admin_helpers.py — URL repo we wszystkich 6 testach,
  POST /v2/statements mock dla testu z dyscyplinami, asercje
  dostosowane do nowych wiadomości (info o sync statements +
  końcowy success zamiast pojedynczej wiadomości)

Helper ``_patch_intended_statements`` w test_client_sync.py —
zduplikowany z test_pbn_test_wysylka_interaktywna.py (świadoma
decyzja — duplikacja lepsza niż cross-file test imports).

Wszystkie 37 testów PBN przechodzą lokalnie.

Uwaga: 3 istniejące testy w pbn_export_queue/tests/test_tasks.py
(test_queue_pbn_export_batch_*) padały i przed moimi zmianami —
pre-existing conflict testdb fixture teardown (TRUNCATE CASCADE +
username unique violation); nie jest to regresja.

* feat(pbn_export_queue): RETRY_LATER handling dla StatementsResendFailedException

Commit 7/8 refaktoryzacji sync_publication.

Gdy sync_publication w górze rzuci ``StatementsResendFailedException``
(POST publikacji do /repositorium OK, ale retry dla GET/DELETE/POST
/v2/statements wyczerpany), kolejka traktuje to jako chwilową
niedostępność PBN — ponawia za kilka minut (RETRY_LATER).

Zmiany:
- ``_handle_retry_exception`` w PBN_Export_Queue.models łapie
  ``StatementsResendFailedException`` przed fallback-iem na TECHNICZNY.
  Komunikat w polu komunikat: "Synchronizacja oświadczeń nie powiodła
  się po wyczerpaniu prób (PBN UID=...): {last_error}. Ponowię wysyłkę
  za kilka minut..."
- Import ``StatementsResendFailedException`` dodany.
- Test ``test_send_to_pbn_statements_resend_failed_exception`` — weryfikuje
  RETRY_LATER + komunikat.

* docs(pbn): changelog + update pbn-wysylka-plan.md dla Fazy 2

Commit 8/8 refaktoryzacji sync_publication. Finalny commit.

- src/bpp/newsfragments/+pbn-sync-publication-split-flow.bugfix.rst
  — changelog towncrier opisujący całą refaktoryzację
- docs/pbn-wysylka-plan.md — sekcja "Faza 2" zaktualizowana ze statusem
  "zaimplementowana", opis docelowego flow, listy zmian modelu,
  nowego wyjątku i historii commitów PR #164

* fix(pbn): NameError releaseDateYear + priorytet openaccess_data_opublikowania

Dwie naprawy w obszarze dat OpenAccess — Commit 9/8 (post-review).

CRITICAL BUG FIX — NameError w convert_js_with_statements_to_no_statements:

Stary kod:
  try:
      i = int(json["openAccess"]["releaseDateYear"])
  except (ValueError, TypeError, AttributeError):
      pass
  json["openAccess"]["releaseDateYear"] = i  # <-- NameError gdy except

Gdy int() failowała (np. PBN zwróci "unknown" albo None zamiast "2022"),
zmienna i była niezdefiniowana, a bezwarunkowy assignment rzucał NameError.
Po refaktoryzacji upload_publication ZAWSZE wywołuje
convert_js_with_statements_to_no_statements (wcześniej tylko warunkowo),
więc bug stał się krytyczny.

Fix: assignment tylko wewnątrz try, except swallowed (wartość
zachowana w oryginalnym formacie — PBN zwróci validation error).

DATE SOURCE FIX — priorytet openaccess_data_opublikowania:

_build_open_access_release_date używało public_dostep_dnia/dostep_dnia
jako źródła releaseDate. Gdy brak → hardcoded releaseDateMonth="JANUARY"
i releaseDateYear=str(rok). Problem: BPP ma dedykowane pole
ModelZOpenAccess.openaccess_data_opublikowania (DateField), ustawiane
przez importer PBN i przez redakcję ręcznie — adapter go ignorował.
Dodatkowo wysyłanie "JANUARY" gdy nie znamy faktycznego miesiąca było
wprowadzaniem PBN w błąd.

Nowy priorytet źródeł:
1. openaccess_data_opublikowania (dedykowane pole OA)
2. public_dostep_dnia (fallback, data wolnego dostępu)
3. dostep_dnia (fallback, data płatnego dostępu)

Gdy żadna data nie istnieje: NIE ustawiamy releaseDate/Month/Year
(zamiast wysyłania kłamliwego "styczeń"). PBN zwróci validation error
z jasnym komunikatem jeśli pole jest wymagane — redakcja uzupełni
brakującą datę w BPP zamiast ufać fałszywym wartościom.

* fix(pbn): POST sync statements wysyła tylko brakujące w PBN (selective mode)

Commit 10 refaktoryzacji sync_publication — post-review fix (Faza 2c).

Problem: w trybie selektywnym (domyślnym) POST oświadczeń do
/api/v2/institution-profile/statements wysyłał PEŁNY zestaw BPP —
także te oświadczenia, które już były w PBN. Kod wywoływał
pbn_get_api_statements() zwracające wszystkie statements z BPP,
bez filtra po only_in_intended.

User wskazał że API PBN może nie być idempotentne — wysyłanie
duplikatów może spowodować błędy walidacji lub zduplikowane rekordy
w PBN. W kroku 4b algorytmu (różnice) powinniśmy wysyłać TYLKO
oświadczenia których nie ma w PBN, nie dublować istniejących.

Zmiany:

- ``_post_statements_with_retry(rec, objectId, publication_pk, filter_keys=None)``
  — dodany opcjonalny parametr ``filter_keys: set | None``. Gdy None,
  POST-ujemy pełen zestaw (zachowanie jak dziś, używane w trybie batch
  po delete_all). Gdy set kluczy ``(personObjectId, disciplineId)``,
  POST-ujemy tylko statements dopasowane do tych kluczy.

- ``_build_post_statements_payload(rec, filter_keys=None)`` — nowy
  helper do budowania payloadu. Dla filter_keys≠None:
  * bierze ``publicationUuid`` z ``adapter.pbn_get_api_statements()``
    (wymuszenie get_pbn_uuid — rzuca ``DaneLokalneWymagajaAktualizacjiException``
    gdy brak V2)
  * bierze surowe statements z ``pbn_get_json_statements()``
  * filtruje po ``_statement_key_intended in filter_keys``
  * konwertuje każdy przez ``_convert_stmt_for_api``

- ``_convert_stmt_for_api`` (nowa staticmethod) — ekstrakcja konwersji
  ``statement → format POST``: usunięcie disciplineId gdy jest disciplineUuid,
  rename type→personRole, usunięcie personNaturalId. Skopiowane z
  ``WydawnictwoPBNAdapter.pbn_get_api_statements._convert_stmt`` (patrz
  komentarz w metodzie — gdy format się zmieni, trzeba poprawić w obu
  miejscach).

- ``_sync_statements_with_pbn`` — dla ``only_in_intended``:
  * kasuj_selektywnie=True → POST z filter_keys=only_in_intended
    (kroki 3 i 4b algorytmu — only_in_intended pokrywa oba scenariusze:
    PBN puste = wszystkie klucze BPP, PBN+BPP różne = tylko brakujące)
  * kasuj_selektywnie=False → POST bez filter_keys (batch: po delete_all
    POST-ujemy wszystko)

Testy (test_client_sync.py):

- ``test_sync_statements_roznice_selektywnie`` — dodana asercja że
  body POST zawiera TYLKO 1 statement (only_in_intended), nie pełen
  zestaw.

- ``test_sync_statements_pbn_subset_bpp_superset_tylko_brakujace`` —
  nowy test: PBN={(A,301)}, BPP={(A,301),(B,200)}. Weryfikuje że
  DELETE nie wywołany, POST zawiera TYLKO (B,200) — nie dubluje
  istniejącego (A,301).

- ``test_sync_statements_pbn_puste_wysyla_wszystkie_w_selektywnym`` —
  nowy test: PBN={}, BPP={A,B}. Weryfikuje że POST wysyła 2 statements
  (wszystkie BPP), mimo filter_keys (bo only_in_intended = wszystkie).

- ``test_sync_statements_batch_mode_post_wszystkie`` — nowy test dla
  batch mode: po delete_all POST wysyła pełen zestaw BPP bez filtra.

Wszystkie 20 testów test_client_sync.py pass. Pełen suite PBN pass
(pre-existing 1 failed + 9 errors w pbn_export_queue/tests/test_tasks.py
niezwiązane z tą zmianą).

* feat(docker): branch alias również w stopce dev buildów

Stopka pokazuje teraz "wersja X (119-merge, feature-nowe-zglos-publikacje,
commit YYYYYYY)" — od razu widać i kanoniczny tag PR-a, i sanityzowaną
nazwę gałęzi. Wcześniej stopka znała tylko jeden alias (final_tag),
więc nie było jasne że można też pullować po nazwie brancha.

Pipeline: workflow przekazuje branch_tag do bake'a, bake do bpp_base
runtime, a tam ENV BPP_BRANCH_TAG zostaje wczytany przez nowy
template tag {% bpp_branch_tag %}. Master release ma BPP_BUILD_FLAVOR=
release → tag zwraca pusty string, stopka pokazuje samą wersję.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
(cherry picked from commit b2837d6)

* feat(pbn): popraw wysylke do repo + audit URL endpointu w SentData

- convert_js_with_statements_to_no_statements -> convert_json_*; funkcja
  faktycznie usuwa klucz `statements` (endpoint /repositorium/publications
  go nie przyjmuje). To naprawia HTTP 400 przy wysylce.
- SentData.api_url: nowe pole zapisuje pelny URL endpointa do ktorego
  wyslano dane — do diagnostyki "gdzie poszlo to ostatnie 400".
- pbn_export_queue: detal zlecenia pokazuje api_url; _build_ai_prompt
  fallbackuje na konfiguracje Uczelni gdy api_url puste.
- cleanup zdublowanego pop("statements") w pbn_test_wysylka_interaktywna.
- testy guarda dla convert_json + MockTransport.base_url zeby fixtury
  dzialaly po dodaniu odwolania do transport.base_url w upload_publication.
- przy okazji: znormalizowane string-fieldy w SentData (exception,
  api_response_status, typ_rekordu, api_url) — null=True -> default="",
  data migration konwertuje istniejace NULL->""; save() przeniesione
  przed custom methods (DJ012).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(docker): wez dev'owa wersje build-docker workflow + usun .docker-build

Po merge'u dev nasz lokalny conflict-free auto-merge zachowal nasza
wersje workflow (tylko master + .docker-build flag mechanism). User
zdecydowal ze chce dev'owa wersje (auto-build na feature/fix/hotfix
przez push trigger) — wiec nadpisuje plikiem z origin/dev i kasuje
.docker-build.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(docker): wywal mechanizm .docker-build z workflow

Plik .docker-build juz nie istnieje (skasowany w poprzednim commicie),
wiec elif sprawdzajacy `[ -f ".docker-build" ]` byl dormantnym kodem.
Zastapione: push na non-master (czyli feature/fix/hotfix przez
restrykcje triggera) → buduj zawsze. Realizuje user-intent "auto-build
na feature branches" — bez tego push na feature spadalby na else
(skip), a `.docker-build` flag nie istnieje.

Komentarze i opisy aktualizowane — bez wzmianek o pliku flagi.
Pozostale `docker-build` w workflow to label PR-a (mechanizm zostaje).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(pbn): migracja 0069 musi byc atomic=False

Production deploy wywala sie:
  psycopg2.errors.ObjectInUse: BLAD: nie mozna ALTER TABLE
  "pbn_api_sentdata" poniewaz posiada oczekujace zdarzenia wyzwalaczy

Wynika z kolejnosci operacji w 0069: RunPython UPDATE NULL→"" generuje
deferred trigger events (denorm/easyaudit), a kolejny AlterField (ALTER
TABLE) w tej samej transakcji nie moze przejsc. atomic=False sprawia,
ze kazda operacja commituje sie osobno: triggery odpalaja po UPDATE,
ALTER startuje czysto.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* ci(docker): buduj PR/feature push tylko gdy actor=mpasternak

Wycofuje gating labelem .docker-build/docker-build na rzecz prostszej
zasady: master/main push i workflow_dispatch buduja zawsze (release
flow + manual override), pozostale (PR sync, feature/fix/hotfix push
bez PR) — tylko gdy actor=mpasternak. Inni contributorzy nie pala
Docker Cloud minutek; jesli trzeba zbudowac obraz dla cudzego PR-a:
`gh workflow run build-docker-images.yml --ref <branch>`.

Dev branch dopisany jawnie do komentarza w pushu jako "intentionally
excluded" — push do dev nie odpala buildu (intermediate state nie
zasluguje na obraz, release leci przez master).

Dodany main do triggerow obok master (gdyby kiedys repo zmienilo
default branch — single source of truth).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(html-minify): zabezpiecz frontend przed regresjami minifikacji HTML

Trzecia regresja w 2 tygodnie (footer wskakiwal pod tabele na
/pbn_export_queue/?page=4#pager) — naprawione + 3 warstwy prewencji
zeby klasa nie wracala.

Hotfixy templatow:
- base.html (linie 240, 266): usunieto self-closing <p/> z globalnego
  base templatu;
- pagination/{pagination,pagination_with_anchor}.html i
  bpp/templates/pagination.html: pusty <li class=ellipsis></li>
  zamieniono na <li class=ellipsis><span>&hellip;</span></li> —
  minify-html traktowal pusty li jako redundant void i usuwal go;
- pagination_with_anchor.html: wyciagnieto inline <style> (96 linii)
  do _pagination.scss — partial jest swap-owany przez htmx
  innerHTML, kazdy swap re-injektowal CSS-a;
- import _pagination w app-{blue,orange,green}.scss.

Prewencja systemowa:
- BppMinifyHtmlMiddleware.should_minify() omija requesty z naglowkiem
  HX-Request: true. minify-html jest zaprojektowane dla pelnych
  dokumentow — na fragmentach htmx rozjezdza DOM swoimi heurystykami.
  To eliminuje cala klase bugow.
- djlint jako linter HTML w pre-commit (--lint, bez auto-reformatu;
  [tool.djlint] enforcuje H020 puste-tag-pair, H025 orphan-tag, H017
  void-self-closing; reszta wyciszona).
- src/bpp/tests/test_html_minify_integrity.py: 5 testow z canary
  HTML-em odwzorowujacym ostatnie incydenty (pusty <li>, <p/>,
  <span/>) + sentinel "self-closing span PSUJE DOM" + bypass-test
  dla htmx + sanity dla non-htmx.

Higiena:
- src/django_bpp/bpp/static/500.html: usuniety z gita (zawiera
  tenant-specific dane z BD jak nazwa uczelni; generowany w entrypoint
  appservera) + dodany do .gitignore.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pbn_export_queue): wskazuj OpenAPI v3 spec w AI prompt

Bylo: ogolny https://pbn.nauka.gov.pl/api/ — strona-rozdroze, AI musial
zgadywac gdzie sa schematy.
Teraz: https://pbn.nauka.gov.pl/api/v3/api-docs — Swagger/OpenAPI JSON
ze wszystkimi endpointami, schematami request/response i kodami bledow.
AI moze pobrac, sparsowac i konkretnie wskazac ktore pole laczy z bledem
schematu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pbn_api): loguj headers i raw body przy 5xx z PBN

500 z `Brak szczegółów błędu` po stronie BPP utrudnia diagnostykę —
`smart_content` mogł gubić body, a response headers nigdy nie były
nigdzie zapisywane. Przy `status_code >= 500` w `_check_error_response`
dorzucam `logger.error` z surową treścią `ret.content[:4000]`,
`ret.headers` oraz `body_len`, żeby było widać czy PBN cokolwiek
zwraca w body i co trafia w nagłówkach (Server, Content-Type itp.).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pbn): przywróć warunkowy wybór endpointu wysyłki publikacji

Cofa standardyzację z commitu 3092d1b (zawsze /v1/repositorium/publications)
i przywraca decyzję o endpoincie na podstawie obecności lokalnych statements
oraz flagi `Uczelnia.pbn_wysylaj_bez_oswiadczen`:

- praca z lokalnymi statements → POST /v1/publications (all-in-one,
  surowy payload z adaptera, statements w body, response `{objectId}`);
- praca bez lokalnych statements + flaga uczelni `=True` →
  POST /v1/repositorium/publications (po `convert_json_with_statements_
  to_no_statements`, body owinięte w listę, response `[{id}]`);
- praca bez lokalnych statements + flaga `=False` → adapter rzuca
  `StatementsMissing` (zachowanie niezmienione).

`sync_publication` po wysyłce ZAWSZE wywołuje `_sync_statements_with_pbn`
(GET /page/statements → diff → DELETE/POST przez /v2/institution-profile/
statements), niezależnie od endpointu wysyłki publikacji. Dla pracy bez
statements lokalnie sync wykasuje ewentualne pozostałości oświadczeń
po stronie PBN; dla pracy z statements typowo no-op (PBN ma już
identyczne z body), ale wykryje dryft jeśli się pojawi.

Zmiany w `publication_sync.py`:
- przywrócono `post_publication()` (POST do `/v1/publications`);
- `_prepare_publication_json` zwraca tuple `(js, bez_oswiadczen)`,
  konwertuje TYLKO gdy `bez_oswiadczen` (endpoint repo wymaga
  `firstName` zamiast `givenNames`, abstracts w root, brak `fee` itp.);
- `_post_publication_data` rozgałęzia po `bez_oswiadczen` (różny
  format response dla obu endpointów);
- `upload_publication` zwraca prawdziwe `bez_oswiadczen` (zamiast
  zawsze `True`) i zapisuje właściwy URL endpointu w `SentData.api_url`.

`pbn_test_wysylka_interaktywna` w KROK 3/8 dorzuca podpowiedź
„Produkcja wybrałaby: [X]" (na podstawie obecności statements) — user
ma nadal ręczny wybór, ale widzi, którą drogą poszłaby produkcja dla
tej pracy.

Testy: nowe scenariusze w `test_client_upload.py` (oba endpointy +
StatementsMissing), `_setup_common_mocks` w `test_client_sync.py`
mockuje OBA endpointy POST publikacji (niezależnie od którą stroną
poszedł upload), `test_sync_publication_zawsze_endpoint_repo`
przebudowane na 2 testy (`bez_statements_idzie_do_repo` +
`z_statements_idzie_do_v1_publications`), `test_bpp_admin_helpers.py`
zaktualizowane (`test_z_oswiadczeniami` i scenariusze UID change/
conflict używają teraz mocka `/v1/publications`).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pbn_api): rollbar + console logger dla diagnostyki API >=400

Dotychczasowy `logger.error` (commit c525c79) odpalał się tylko dla
5xx, ale i tak nie pojawiał się na konsoli — w `LOGGING` w
`django_bpp/settings/base.py` jedynym skonfigurowanym loggerem był
`pbn_import` (handler `console`, propagate=False). Logi z
`pbn_api.client.transport` ginęły bo nikt ich nie zbierał.

Dwie zmiany:

1. `_check_error_response` w `transport.py`:
   - `logger.error` rozszerzony z `>= 500` na `>= 400` (przy 4xx też
     warto widzieć body i headers — np. „400 Bad Request" bez
     czytelnego body to typowa odpowiedź PBN przy validation errors);
   - DODATKOWO `rollbar.report_message` przy każdym >= 400, z
     `extra_data` (status_code, url, headers, body_len, body skrócone
     do 4000 znaków przez `smart_content`). Level: `error` dla 5xx,
     `warning` dla 4xx.

2. `LOGGING` w `settings/base.py`: dodany logger `pbn_api`, level
   `WARNING`, handler `console`, `propagate: False`. Teraz
   `logger.error` z transport.py faktycznie pojawi się na stderr
   (runserver, Celery worker, manage.py commands).

`423 Locked` nadal jest specjalnie obsłużone (raise
`ResourceLockedException` PRZED log/rollbar) — to nie błąd
diagnostyczny, tylko sygnalizacja zajętego zasobu.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* feat(pbn): kasuj oświadczenia z PBN PRZED POST do /v1/repositorium

Sytuacja docelowa: praca lokalnie ma już 0 dyscyplin (np. ostatnia
została skasowana z `Wydawnictwo_*_Autor`), a po stronie PBN nadal
wiszą stare oświadczenia. POST do `/v1/repositorium/publications`
może odrzucić publikację gdy ma już oświadczenia w PBN — kasujemy
je upfront, nie czekając na post-upload sync.

Nowy helper `_pre_upload_clear_pbn_statements_if_any(rec)` w
`PublicationSyncMixin`:

- Brak `pbn_uid_id` → no-op (PBN nie ma jeszcze odpowiednika pracy).
- GET `/page/statements` z PBN. Failure → log warning, kontynuuj.
  POST może rzucić czytelny błąd jeśli problem rzeczywiście blokuje
  wysyłkę — nie chcemy blokować całej wysyłki na błędzie GET.
- PBN zwraca pustą listę → no-op.
- PBN ma oświadczenia → DELETE selektywnie/batch wg
  `Uczelnia.pbn_kasuj_dyscypliny_selektywnie` (reuse istniejących
  helperów `_delete_statements_selective` / `_delete_statements_batch`).
  DELETE failure rzucamy w górę (`StatementsResendFailedException`) —
  nie wysyłamy publikacji wiedząc że PBN za chwilę odrzuci.

Wywołanie w `upload_publication` warunkowe na `bez_oswiadczen=True`
(czyli ścieżka /v1/repositorium/publications). Dla /v1/publications
(all-in-one z statements w body) pre-clear jest pomijany —
ewentualny drift wykrywa post-upload `_sync_statements_with_pbn`.

4 testy w `test_client_upload.py`:
- DELETE selektywny przed POST gdy PBN ma statements + BPP puste;
- pomija (no DELETE) gdy PBN puste;
- pomija (no GET, no DELETE) gdy brak pbn_uid_id;
- pomija (no GET) dla ścieżki /v1/publications.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* Merge

* Dodano exponential backoff dla pobierania PublikacjaInstytucji_V2

Funkcja _download_statements_with_retry() teraz ponawia pobieranie
V2 API 5 razy z rosnącym czasem oczekiwania (2s, 4s, 8s, 16s, 32s).
Poprzednio jedyna próba kończyła się niejasnym warningiem "nie jest błędem",
co myliło użytkowników - brak V2 oznacza brak UUID i brak możliwości
generowania linków do PBN Interfejs oraz wysyłki oświadczeń.

Po wyczerpaniu prób wyświetlany jest BŁĄD z sugestią użycia
PBN Export Queue (wysyłka w tle) zamiast interaktywnej.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* Fix workflows

* fix(migrations): scal liście grafu bpp (0417_merge+0417_remove → 0418) po merge dev

Merge dev wniósł 0417_remove_uczelnia_pokazuj_raport_autorow_and_more
obok istniejącej 0417_merge_20260601_0632. Pusta migracja scalająca
unifikuje graf — bez zmian schematu.

* fix(pbn_api): importuj MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA z fixtures.pbn_api

test_PBNClient_post_publication_no_statements importowal stala z roota
pakietu (`from fixtures import ...`), ale po merge'u z dev `fixtures/__init__.py`
celowo NIE re-eksportuje juz symboli `MOCK_*` (eager import pociaga modele
Django -> AppRegistryNotReady przy preloadzie conftestu sprzed django.setup()).
Konwencja udokumentowana w fixtures/__init__.py: `from fixtures.pbn_api import`.
Scalam z sasiednim importem pbn_pageable_json, zgodnie ze stylem reszty pliku.

Failed run: https://github.com/iplweb/bpp/actions/runs/26742374664

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(tests): deterministyczny wybór w Select2 helperze — klik pasującej pozycji zamiast Enter (#274)

select_select2_autocomplete wciskał Enter wybierając PIERWSZĄ podświetloną
pozycję wyników. Pod obciążeniem CI press_sequentially wpisuje znaki wolniej
niż debounce Select2, więc dla "Aut2…" w oknie czasowym widoczne bywają jeszcze
wyniki dla "Aut…" — zawierające INNEGO autora o wspólnym prefiksie. Enter
po cichu wybierał złego autora; błąd ujawniał się dopiero przy zapisie jako
UniqueViolation na (rekord_id, autor_id, typ_odpowiedzialnosci_id), bo oba
wiersze formsetu wskazywały na ten sam autor_id.

Helper klika teraz konkretną pozycję, której widoczny tekst zawiera pełną
szukaną wartość; tylko gdy takiej pozycji nie ma (pola, gdzie szukany tekst
nie jest podłańcuchem etykiety, np. generowane warianty "zapisany jako")
spada do dawnego Enter-na-pierwszej.

Weryfikacja lokalna: test_..._dwoch_autorow × 3 parametry × 3 powtórki = 9/9
passed.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(deps): bump django-multiseek 0.9.49 → 0.10.2 (#273)

0.10.1 (as proposed in the grouped Dependabot bump #264) shipped a wheel
with NO static/ directory: its package-data globs were non-recursive
("static/multiseek/*.js") and didn't match the real layout
("static/multiseek/js/multiseek.js"). Downstream, /static/multiseek/js/
multiseek.js and style.css returned HTTP 500, so formAsJSON /
multiseekFrame were undefined and the search form died — surfacing as
Playwright navigation timeouts in test_playwright/test_multiseek.py.

0.10.2 fixes the packaging upstream (recursive globs); the published wheel
now ships the assets again. CSRF hardening introduced in 0.10.x is
unaffected and BPP's own live-results/results views remain csrf_exempt.

Verified locally on this branch (latest dev → denorm 1.11.1 + ON CONFLICT
fix from #270): test_playwright/test_multiseek.py is 6/6 green.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(static): cache-busting długiego ogona statyków przez Manifest (#272)

Realizuje ustalenia spike'u z #269 (follow-up #267).

Dodaje TolerantManifestStaticFilesStorage — subklasę
ManifestStaticFilesStorage, która hashuje każdy {% static %} z osobna
(name.<content-hash>.ext) i przepisuje odwołania url()/sourceMappingURL
w CSS/JS. Pokrywa cały long-tail statyków, którego {% compress %} (#267)
nie umie: pliki z defer/async, dane ładowane fetch-em (bpp/js/Polish.json),
vendored singletony (multiseek.js, select2-autofocus.js).

Dlaczego subklasa, nie vanilla: vanilla wywala collectstatic na pierwszym
nierozwiązywalnym odwołaniu — url()/sourceMappingURL wskazującym na plik
spoza zebranych statyków (~132 vendored-refy: sprite'y .png grappelli/
jqueryui + sourcemapy .map foundation, których targetów nie ma w
STATIC_ROOT). Subklasa łapie ten ValueError w hashed_name i zostawia
odwołanie pod nazwą oryginalną — te pliki serwują się jak dziś, tylko bez
cache-bustingu (i tak ~nigdy się nie zmieniają). manifest_strict=False
dokłada runtime'ową tolerancję dla brakujących wpisów (chroni też dev/testy
bez collectstatic; w dev działa dodatkowo short-circuit DEBUG w .url()).

Zakres (Additive only): zero zmian w blokach {% compress %}, szablonach i
entrypoincie — Manifest pokrywa long-tail, compress dalej bundluje hot-path.
Kontrakt staticów bez zmian: entrypoint `cp -rf /app/staticroot.baked/.`
kopiuje cały .baked, więc staticfiles.json jedzie razem automatycznie.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(migrations): scal liście grafu bpp (0418 trigger + 0418_merge → 0419) po merge dev

Merge dev wniósł 0418_autor_dyscyplina_trigger_on_conflict (#270),
co stworzyło drugi liść obok 0418_merge_20260601_0954 z tej gałęzi.
Pusta migracja scalająca przywraca pojedynczy liść grafu.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant