feat(soft-delete): faza 03 — audyt kategorii B (import i scalanie a kosz) - #742
Conversation
…(failing) Test bazowy fazy 03. Rekord_w_bpp matchuje przez Rekord, czyli widok bpp_rekord_mat przefiltrowany po deleted_at juz w fazie 01 -- skasowana publikacja z niego znika, matching zwraca None, a importer tworzy DUPLIKAT. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
get_bpp_publication i rekord_w_bpp matchowaly przez Rekord, czyli widok bpp_rekord_mat przefiltrowany po deleted_at juz w fazie 01. Soft-skasowana publikacja z niego znika, wiec matching zwracal None, importer zakladal brak rekordu i tworzyl DUPLIKAT. Teraz odpytujemy modele zrodlowe przez global_objects. Patent NIE ma pola pbn_uid -- sprawdzone na modelach, nie zalozone. Wpisanie go do listy wywalilo by FieldError przy pierwszym .filter(), bo Django resolwuje nazwy pol natychmiast, a nie przy iteracji. Zachowana historyczna ROZNICA miedzy tymi dwiema metodami: przy wielu trafieniach get_bpp_publication zwraca None, a rekord_w_bpp sklejone tytuly; przy braku trafien rekord_w_bpp spada do fuzzy matchingu. Nie ujednolicam tego przy okazji. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…sk 1b) ZMIANA DECYZJI #14 planu fazy 03, rozstrzygnieta przez wlasciciela systemu. Plan zakladal "POMIN + ZARAPORTUJ", argumentujac, ze auto-restore pozwolilby importowi wskrzeszac rzeczy skasowane celowo. Wlasciciel rozstrzygnal inaczej: PBN jest zrodlem prawdy dla tych publikacji, wiec skoro rekord tam jest, ma wrocic takze do BPP. Ryzyko wskazane w tamtej decyzji nie znika -- zmienia postac z "nie robmy tego" na "robmy, ale zostawmy slad". Tym sladem jest nowy model RekordPrzywroconyPrzezImport (pbn_integrator, migracja 0001): rekord (generic FK, bo import dotyka kilku modeli), data, publikacja PBN i sciezka importu. Operator, ktory znajdzie w bazie publikacje skasowana przez siebie tydzien wczesniej, ma gdzie sprawdzic, ze wrocila z importu, kiedy i skad. Wpis powstaje WYLACZNIE przy realnym wskrzeszeniu -- zwykly re-import zywego rekordu nie zostawia nic. Inaczej rejestr zapelnilby sie szumem i przestal cokolwiek znaczyc; przypina to osobny test. Helper przyjmuje takze wejscie NIE-modelowe bez wyjatku: rekord_w_bpp zwraca STRING ze sklejonymi tytulami przy wielu trafieniach po pbn_uid (zachowanie historyczne, utrzymane w Tasku 2) albo None. Helper stoi na sciezce importu, wiec wywrocenie przebiegu z tego powodu byloby regresja niezwiazana z koszem. Wpiete w trzy wejscia importera: articles, books, chapters. Zweryfikowane mutacyjnie: wylaczenie warunku na deleted_at wywala test o wskrzeszeniu i rejestrze (pierwsza czerwien byla ImportError, wiec nie dowodzila, ze asercje cokolwiek pilnuja). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…zji #14 w planie matchuj_publikacje i helpery (_try_match_pub_by_doi/zrodlo/isbn/uri/title) odpytywaly klass.objects, wiec fuzzy matching nie widzial soft-skasowanych rekordow -- re-import uznawal je za nieistniejace i tworzyl DUPLIKAT, ktorego operator nie zauwazy, bo oryginal siedzi w koszu. Wprowadzony helper widzacy_manager(klass) z getattr-fallbackiem, bo klass bywa takze Rekord: to widok (managed=False), nie SoftDeleteModel, wiec nie ma global_objects -- za to jest odfiltrowany po deleted_at juz na poziomie SQL-a (faza 01), czyli "menedzer widzacy" dla niego nie istnieje. ODSTEPSTWO OD PLANU, swiadome: plan wymienial linie 87/108/181/235/249, pomijajac _build_isbn_query. Zmienilem tam rowniez baze zapytania, bo uzasadnienie jest identyczne -- re-import ksiazki matchowanej po ISBN tworzylby duplikat tak samo. Na objects zostaja WYLACZNIE dwa wywolania wydawnictwa_nadrzedne_dla_innych(): to metoda menedzera domenowego, ktorej GlobalManager pakietu nie ma, a plan swiadomie akceptuje liczenie nadrzednych sposrod nieusunietych. Test po DOI mial blad PO MOJEJ STRONIE: podawal celowo inny tytul, a _try_match_pub_by_doi mimo zawezenia po DOI przepuszcza kandydata przez prog podobienstwa tytulu (0.80). Test padal wiec z powodu reguly matchingu, nie widocznosci kosza. Poprawiony, zeby mierzyl to, co mial mierzyc. Plan fazy 03: decyzja #14 oznaczona jako ZMIENIONA (PRZYWROC + ODNOTUJ), z zachowaniem pierwotnego rozumowania -- bo to ono uzasadnia istnienie rejestru. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
… 5, 6) TASK 5. Matching ksiazki-matki rozdzialu (chapters.py) szukal przez Wydawnictwo_Zwarte.objects, wiec skasowanej ksiazki nie widzial: importer uznawal ja za nieistniejaca i importowal PONOWNIE. Powstawal duplikat ksiazki, a rozdzialy odpinaly sie od oryginalu. Lookup wydzielony do znajdz_ksiazke_nadrzedna() -- glownie po to, zeby dalo sie go przetestowac bez budowania calego JSON-a z PBN. Trafienie w kosz oznacza wskrzeszenie, zgodnie z decyzja z Taska 1b; wpis w rejestrze dostaje osobne zrodlo "chapters:ksiazka-nadrzedna", zeby dalo sie odroznic wskrzeszenie ksiazki od wskrzeszenia samego rozdzialu. Kontrakt zachowany: brak trafienia to nadal None (a nie wyjatek), bo wolajacy ma wtedy zaimportowac ksiazke z PBN. Przypiete testem. TASK 6. _delete_existing_publications w pbn_import mial dac czysty stan przed pelnym re-importem, ale po fazie 02 QuerySet.delete() jest MIEKKIE. Zmienione na hard_delete() ORAZ global_objects -- oba potrzebne z roznych powodow: - hard_delete(), bo inaczej publikacje zostaja w bazie i "czyszczenie" nie czysci; - global_objects, bo objects nie widzi kosza, wiec publikacje PBN skasowane wczesniej przez operatora przezylyby czyszczenie, a potem re-import by je wskrzesil. Operator zobaczylby z powrotem rekordy, ktore usunal, i to bez sladu, ze przezyly czyszczenie. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
ruff format na calym katalogu pbn_import zagarnal fix_import_dat_oswiadczen_pbn.py, ktorego faza 03 nie dotyka. Ta sama lekcja co w fazie 02: formatowac TYLKO wlasne pliki, inaczej PR robi sie nieczytelny. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
… decyzji (Task 7, 8) TASK 7. Jedyna realna zmiana w tym tasku: transfer w deduplikator_autorow/utils/merge.py. Cytowane w planie linie byly NIEAKTUALNE (plik zrefaktoryzowany -- ostrzegal o tym handoff fazy 02), wiec lookupy odnalezione na nowo: dwa miejsca, jedno dla publikacji (Praca_*), jedno dla through-modeli. Bez widzenia kosza autorstwa i prace soft-skasowane zostawaly przy autorze-duplikacie, ktory po scaleniu ma zniknac -- powstawaly SIEROTY. W fazie 04 zablokowalyby dodatkowo guard PROTECT, wiec problem ujawnilby sie dopiero tam, w miejscu niezwiazanym z przyczyna. Reszta Taska 7 to decyzje "ZOSTAW objects", zapisane jako rejestr w pliku testowym (ewaluacja, snapshot odpiec, komparator PBN, REST API). Sprawdzone przy okazji: wszystkie .update() w tych miejscach dotycza przypieta / dyscyplina_naukowa / afiliuje, a NIE deleted_at -- gate BppSoftDeleteQuerySet.update() z fazy 01 sie tam nie odpala. TASK 8. Skanowanie do dedupu publikacji ZOSTAJE na objects i dostaje test. Rekord w koszu nie jest duplikatem do rozstrzygniecia; podsuwanie go operatorowi kazaloby mu scalac rzecz, ktora sam usunal. To decyzja ODWROTNA niz przy matchingu importu -- i wlasnie dlatego jest przypieta testem, zamiast zostac "oczywista". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Najwazniejsze, co niesie: zmiana decyzji #14 (PRZYWROC + ODNOTUJ zamiast POMIN), szesc faktow o kodzie, ktore w fazie 03 kosztowaly runde poprawek (m.in. Patent bez pbn_uid, rekord_w_bpp zwracajacy STRING, prog podobienstwa tytulu przy matchingu po DOI), oraz lista tego, co czeka faze 04 -- w tym odwrotne OneToOne omijajace soft-delete, na ktore faza 04 trafi ponownie, bo dotyka tej samej relacji. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Handoff byl skupiony wylacznie na fazie 04. Dopisana pelna mapa 04-08 wraz z zaleznosciami (kolejnosc nie jest dowolna: 06 konsumuje kolejke z 05, 07 konsumuje log z 06) oraz lista tego, co fazy 01-03 juz pod nie podlozyly -- sygnatury delete/restore z user/reason, emitowane sygnaly i kontrakt ostatnio_zmieniony. Odnotowane osobno: NAGROBKI na zewnatrz (OAI-PMH status=deleted, CERIF, API) sa nadal NIEZROBIONE, mimo ze handoff fazy 02 traktowal je jako bramke wydania. Fundament gotowy, brakuje ekspozycji; nie naleza do zadnej z faz 04-08, wiec bez tego wpisu przepadlyby miedzy fazami. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Co dalejZostało pięć faz. Kolejność nie jest dowolna — każda kolejna konsumuje coś, co dostarcza poprzednia.
Co ten PR (i poprzednie) już pod nie podłożyłTrzy rzeczy są gotowe — kolejne fazy mają je skonsumować, nie projektować od nowa:
|
… fazy 05 DWIE DECYZJE WLASCICIELA, 2026-08-08. 1. BRAMKA WYDANIA PRZESUNIETA Z FAZY 04 NA 07. Rekomendacja faz 01-03 brzmiala "wydac po 04" i byla uzasadniona bezpieczenstwem -- guardy zapobiegaja osieroceniu. Ale waskim gardlem nie jest bezpieczenstwo, tylko spojnosc dla operatora. Sprawdzone na kodzie: src/bpp/admin/ nie ma DZIS zadnego kosza. Trafienia deleted_at to hydraulika -- ukryte pole formularza dla walidacji ograniczen i filtry agregatow -- a nie filtr "pokaz skasowane" ani akcja "Przywroc". Jednoczesnie SoftDeleteQuerySet.delete jest juz miekki, a delete_selected Django wola wlasnie queryset. Wydanie po fazie 04 daloby wiec operatorowi: "Usun" przestaje znaczyc "zniknelo", rekordu nie da sie przywrocic ani obejrzec bez programisty, i nie da sie go usunac naprawde. Traci jedna zdolnosc i nie dostaje zadnej w zamian, bo kosz przychodzi dopiero w fazie 07. To regresja UX, nie funkcja. Fazy 01-06 scalamy dalej, ale nie wydajemy. Swiadomie odrzucone: wydanie 01+02 z tymczasowym "Usun = twardo" w adminie -- oznaczaloby dwie semantyki kasowania rownolegle i wyrzucenie tego kodu w fazie 07. 2. NAGROBKI WCIAGNIETE DO FAZY 05: OAI-PMH status=deleted, CERIF, sposob odkrycia usunietych w API. To ten sam motyw co wycofanie oswiadczen z PBN -- systemy zewnetrzne dowiaduja sie, ze cos zniknelo. Trzymane osobno przepadlyby miedzy fazami, bo zaden plan ich nie obejmowal, a handoff fazy 02 traktowal je jako bramke wydania. Fundament gotowy: soft-delete bumpuje ostatnio_zmieniony, wiec lista nagrobkow to deleted_objects.filter po tym polu. Brakuje samej ekspozycji. Termin: przed faza 07, nie przed 04. Faza 05 przemianowana na "propagacja usuniecia na zewnatrz". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Zaktualizowane decyzje (2026-08-08)Dwie rzeczy z notatki „co dalej” zostały rozstrzygnięte i zapisane w repo (commit 1. Bramka wydania: faza 04 → faza 07Rekomendacja faz 01–03 brzmiała „wydać po 04”. Zmieniona. Powodem nie jest bezpieczeństwo (guardy z 04 domykają je w porządku), tylko spójność dla operatora. Sprawdzone na kodzie: Wydanie po fazie 04 dałoby więc operatorowi: „Usuń” przestaje znaczyć „zniknęło”, rekordu nie da się przywrócić ani obejrzeć bez programisty, i nie da się go usunąć naprawdę. Traci jedną zdolność, nie dostając żadnej w zamian — kosz przychodzi dopiero w fazie 07. To regresja UX, nie funkcja. Fazy 01–06 scalamy dalej do Świadomie odrzucone: wydanie 01+02 z tymczasowym „Usuń = twardo” w adminie — dwie semantyki kasowania równolegle i wyrzucenie tego kodu w fazie 07. 2. Nagrobki wciągnięte do fazy 05OAI-PMH Fundament gotowy — soft-delete bumpuje |
soynatan/django-easy-audit#348, z forka mpasternak/django-easy-audit. Dopisany komentarz do ich issue #175 z jedyna nowa przeslanka wobec czterech poprzednich prob: obejscie sugerowane przez maintainera przez CRUD_DIFFERENCE_CALLBACKS jest NIEOSIAGALNE, bo wyjatek leci w linii 102, a callbacki sa wolane dopiero w 113. W handoffie zapisane, co zrobic po scaleniu: skasowac easyaudit_shim.py, wywolanie zainstaluj() w BppConfig.ready() i test_easyaudit_shim.py. Przypomni o tym test_upstream_nadal_ma_blad_czyli_shim_jest_potrzebny, ktory wtedy zacznie padac jako XPASS. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…a admina DECYZJA WLASCICIELA 2026-08-08. Soft-delete znaczy, ze rekordu NIE MA. Akcesor, ktory zwraca rzecz skasowana, lamie kontrakt "Rekord albo None", na ktorym stoja wszyscy jego konsumenci. COFNIETE W CALOSCI: - pbn_api/models/publication.py -- rekord_w_bpp i get_bpp_publication wracaja do pytania Rekord. Moja wersja zwracala model KONKRETNY, przez co admin PBN wywalal sie AttributeError na CALEJ changeliscie: admin/publication.py:183 wola .original, a to cached_property istniejaca wylacznie na Rekord. Ten sam blad cicho psul szablon change_form.html (Django polyka brak atrybutu w szablonie -> pusty href, objaw gorszy niz wyjatek). - import_common/core/publikacja.py -- caly widzacy_manager. Fuzzy matching tez nie ma widziec kosza. Przy okazji znika blad, ktorego nie zauwazylem: drugim wolaczem matchuj_publikacje jest deduplikator_publikacji (tasks.py:187), ktory przekazuje modele KONKRETNE -- wiec dedup zaczal podsuwac operatorowi rekordy z kosza, wbrew decyzji z Taska 8. Moj wlasny test tego nie zlapal, bo pokrywal tylko polowe skanujaca (_get_publications_to_scan), a nie matchujaca. Zaglada do kosza IMPORTER, nie akcesor -- to jego decyzja i ma siedziec w sciezce importu. TESTY: dwa testy matchingu skasowane (sprawdzaly juz-bledne zachowanie), test akcesora ODWROCONY na kontrakt "skasowane = nieznalezione". DOLOZONY test_admin_rekord_w_bpp.py -- ta metoda admina nie miala ZADNEGO pokrycia, dlatego bloker wyszedl dopiero w recenzji. Zweryfikowany mutacyjnie: przywrocenie zepsutej wersji wywala oba testy na AttributeError 'Wydawnictwo_Ciagle' object has no attribute 'original'. Znika przy okazji ustalenie #8 z recenzji: nowy przypadek "wiele trafien" byl skutkiem iterowania po czterech modelach zamiast pytania Rekord. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Sesja przerwana w polowie naprawiania. PR #742 NIE nadaje sie do scalenia. Blokery 1 i 2 (padniety admin PBN, cichy pusty href w szablonie) naprawione commitem eb04689. Zostaje 5 pozycji, opisanych z file:line i kolejnoscia, plus 5 ustalen recenzji o nizszym priorytecie. Handoff zawiera dwie decyzje wlasciciela, ktore ustawiaja reszte pracy: soft-delete znaczy ze rekordu NIE MA -- akcesor zwraca Rekord albo None, a zagladanie do kosza jest decyzja IMPORTERA; oraz wariant A dla trafienia w kosz (PRZYWROC + ODNOTUJ). Osobna sekcja z pulapkami, ktore juz kosztowaly -- w tym najwazniejsza: lista wolaczy to nie to samo co lista zalozen wolaczy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…one (3.1) Zajrzenie do kosza siedzialo w galezi ``ret is not None``, gdzie po cofnieciu akcesorow (commit eb04689) bylo z definicji no-opem: ``rekord_w_bpp`` zwraca tam ``Rekord``, a ``Rekord`` nie ma pola ``deleted_at``. Realny problem jest w galezi przeciwnej. Kosz jest jednoczesnie: - NIEWIDOCZNY dla matchingu -- ``Rekord`` to widok odfiltrowany po ``deleted_at``, wiec ``rekord_w_bpp`` zwraca ``None``, - WIDOCZNY dla bazy -- ``pbn_uid`` to ``OneToOneField(unique=True)`` BEZ warunku partial, wiec rekord w koszu nadal to pole trzyma. Import wchodzil wiec w galaz "utworz nowy" i wywalal sie IntegrityError-em na ``bpp_wydawnictwo_{ciagle,zwarte}_pbn_uid_id_*_uniq``. To nie cichy duplikat, tylko wywrocenie calego przebiegu. Nowe ``znajdz_lub_wskrzes_rekord()`` (preambula wspolna dla trzech importerow) najpierw pyta akcesor o rekord zywy, a gdy go nie ma -- jawnie szuka w koszu po ``pbn_uid`` i wskrzesza (wariant A: PBN jest zrodlem prawdy, slad w rejestrze ``RekordPrzywroconyPrzezImport``). Model jest parametrem, a nie petla po wszystkich publikacjach, bo ``Patent`` nie ma ``pbn_uid`` i ``.filter()`` na nim wywala ``FieldError``. Testy: 5 nowych, na poziomie realnych funkcji importera. Czerwien przed zmiana to IntegrityError z bazy. Zweryfikowane mutacyjnie -- kazda z czterech mutacji (deleted_objects->objects, restore bez rejestru, znajdz bez restore, zle zrodlo_importu) wywraca dokladnie te asercje, ktore ma wywracac. Refs #742 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
BLOKER 3 z self-review. ``Praca_Habilitacyjna.autor`` bylo ``OneToOneField``,
czyli twarde ``UNIQUE (autor_id)``, ktore kosza NIE zna. Scalanie autorow
przenosi wiersze RAZEM Z KOSZEM (zeby nie zostawiac sierot), wiec duplikat
z habilitacja soft-skasowana + glowny autor z zywa wywracal CALE scalanie:
bpp_praca_habilitacyjna_autor_id_b83969d3_uniq
Zamiana na ``ForeignKey`` + ``UniqueConstraint(fields=["autor"],
condition=Q(deleted_at__isnull=True))`` -- ten sam wzorzec, co faza 01
zastosowala do ``*_Autor``. Django wymusza ``unique=True`` w
``OneToOneField.__init__``, wiec zdjecie bezwarunkowego UNIQUE wymaga zmiany
typu pola.
Konsekwencje zmiany typu (sprawdzone, nie tylko "kto wola", ale "co robi
z wynikiem"):
- Akcesor odwrotny to teraz ``autor.praca_habilitacyjna_set`` (manager).
Sciezka FILTROWANIA w ORM/DjangoQL sie NIE zmienia -- ``related_query_name``
domyslnie i tak jest nazwa modelu.
- ``RokHabilitacjiView`` pyta teraz jawnie ``Praca_Habilitacyjna.objects``.
Przy okazji znika reczny warunek na ``deleted_at``: odwrotne OneToOne szlo
przez ``_base_manager`` i pokazywalo rekordy z kosza, manager relacji nie.
- ``browse/autor.html`` idzie petla po ``praca_habilitacyjna_set``, jak juz
robil to doktorat (ktory od zawsze jest FK). Bez tego sekcja "Stopnie
naukowe" zniknelaby CICHO -- szablony Django polykaja brak atrybutu.
Oba te miejsca mialy juz testy w repo i oba zaswiecily sie na czerwono.
Prawdziwy konflikt (obie habilitacje zywe) konczy sie teraz
``KonfliktScalania`` z komunikatem "Nie mozna scalic autorow: obaj maja prace
habilitacyjna." zamiast IntegrityError. Nowy wyjatek jest lapany osobno --
sprzeczne dane to nie awaria, wiec nie ida do Rollbara.
Walidacja w adminie: ``validate_unique()`` juz tego nie pilnuje, a
``validate_constraints()`` CICHO POMIJA constraint, ktorego pole warunku
(``deleted_at``) jest wykluczone z walidacji -- a admin wyklucza wszystko spoza
``fieldsets``. Sztuczka z ukrytym polem (``bpp/admin/core.py``) dziala dla
inline'ow; tutaj wyprodukowalaby widoczny pusty wiersz "Deleted at", wiec jest
jawne ``clean_autor()``. Constraint zostaje ostateczna gwarancja w bazie.
Testy: 6 nowych (3 scalanie, 3 admin). Kazda z czterech mutacji wywraca
dokladnie ten test, ktory ma wywracac; bezwarunkowy unique zostal
udokumentowany przez oryginalna czerwien.
Refs #742
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…go do PBN (3.3, 3.4) 3.3 -- detekcja kolizji autorstw szla po `model.objects`, czyli po menedzerze ZYWYCH. Gdy publikacja jest w koszu, oba autorstwa (glownego i duplikatu) tez tam sa, wiec kolizji nie bylo widac: transfer przechodzil i w koszu ladowaly DWA wiersze `(rekord, glowny, typ)`. Warunkowy `wc_autor_uniq_rekord_autor_typ` obowiazuje wsrod zywych, wiec kolizja wybuchala dopiero przy `publikacja.restore()` -- kosz stawal sie drzwiami jednokierunkowymi. Detekcja idzie teraz po `global_objects` (ten sam `getattr`-fallback, co `wiersze_do_transferu`, bo funkcja obsluguje tez modele bez soft-delete). Uwaga na przyszlosc, ujawniona przy weryfikacji mutacyjnej: wiersz duplikatu znika dzis dlatego, ze `autor_duplikat.delete()` kaskaduje TWARDO po FK. Pierwsza wersja poprawki odpinala go dodatkowo od grupy restore'u wlasnym `transaction_id` -- mutacja pokazala, ze to martwy kod, wiec zostal usuniety. Gdy faza 04 zmieni kasowanie autora na miekkie, ta galaz bedzie musiala wrocic; zapisane w handoffie fazy 04. 3.4 -- scalanie kolejkowalo do `PBN_Export_Queue` wszystko, co przenioslo, takze rekordy w koszu. To sprzeczne z kierunkiem fazy 05: soft-delete ma oswiadczenia z PBN WYCOFYWAC, a nie wysylac tam rzeczy, ktorych w BPP "nie ma". Guard `_w_koszu()` w obu miejscach kolejkujacych (autorstwa + proste publikacje). Testy: 5 nowych. Kazda z trzech mutacji (detekcja po `objects`, zdjecie guardu PBN w kazdym z dwoch miejsc) wywraca dokladnie swoj test. Refs #742 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
- §1: baseline jest niesswiezy (bpp/0487 vs 0500) i byl taki juz przed faza 03; odswiezenie przy scalaniu, nie w feature-branchu - §2a (nowe): druga zmiana kierunku -- akcesory NIE zagladaja do kosza - §3: tabela zmian przepisana na stan faktyczny (poprzednia opisywala implementacje cofnieta w eb04689) + sekcja "Czego faza 03 NIE naprawia" - §4: dwie pulapki o wolaczach akcesorow (`.original` w adminie, `matchuj_publikacje` z dwoma wolaczami o sprzecznych potrzebach) - §5: ostrzezenie dla fazy 04 -- `scal_autora` polega dzis na TWARDEJ kaskadzie `autor_duplikat.delete()`; po zmianie na miekkie kasowanie autora wiersz przetrwa jako sierota i zepsuje `restore()` - §6: cztery pozycje dlugu z handoffu domkniecia (admin rejestru, force, Oswiadczenie_Instytucji, post_hard_delete) - §7: trzy nowe wnioski procesowe -- mutacja ktora PRZESZLA to informacja; dane referencyjne z baseline nie sa w testach gwarantowane; `git checkout` kasuje niezacommitowana implementacje przy testowaniu mutacyjnym Handoff domkniecia oznaczony jako historyczny, z mapowaniem pozycji na commity. Refs #742 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…iki suity Workflow `Tests` ma `pull_request: branches: [dev]`, wiec PR #742 (`feat/soft-delete-03` -> `feat/soft-delete`) dostaje wylacznie GitGuardiana. Zielony check na takim PR-ze NIE jest dowodem, ze testy przeszly -- dotyczy to tak samo faz 04-07, dopoki sa stackowane na `feat/soft-delete`. Dopisany wynik lokalnego `make tests` dla fazy 03: 9522 + 157 + 81 passed, 0 failed. Refs #742 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
…kupu (#9, #13) #9 -- rejestr osiagalny wylacznie przez `manage.py shell` nie spelnial swojej roli. To jego DOSTEPNOSC dla operatora byla calym uzasadnieniem zmiany decyzji #14 planu ("nie robmy tego" -> "robmy, ale zostaw slad"). Slad, do ktorego nie da sie zajrzec, sladem nie jest. - `RekordPrzywroconyPrzezImportAdmin`, TYLKO DO ODCZYTU: trzy `has_*_permission` zwracaja False, bo rejestr, w ktorym operator moze dopisac albo skasowac wpis, przestaje byc dowodem. Samo `readonly_fields` by nie wystarczylo -- chowa pola, ale wciaz pozwala dodac i skasowac wiersz. - Indeks `pbnint_przywr_ct_objid_idx` na `(content_type, object_id)` (migracja `pbn_integrator/0002`). Istniejacy `(przywrocono DESC, content_type)` obsluguje "co wrocilo ostatnio", ale NIE "czy TEN rekord wrocil?" -- a to jedyne pytanie zadawane wprost o konkretna publikacje. #13 -- `Oswiadczenie_Instytucji.get_bpp_publication` (INNY model niz `Publication.get_bpp_publication`, ta sama nazwa metody) nie byl przy audycie rozstrzygniety w ogole. Rozstrzygniecie: ZOSTAJE na `objects`, bo to akcesor, a zagladanie do kosza jest decyzja importera. Nie jest to martwy przepis -- lancuch wolaczy realizuje wariant A w calosci: akcesor zwraca None -> `statements.py:404` wola `importuj_publikacje_instytucji` -> `znajdz_lub_wskrzes_rekord` wskrzesza i wpisuje do rejestru. Wolacz przyjmuje oba ksztalty wyniku (`Rekord` przez `.original`, model konkretny wprost). Gdyby akcesor sam zagladal do kosza, importer nigdy by sie nie odpalil. Testy: 5 nowych (4 rejestr, 1 rejestr decyzji). Test rejestru decyzji przechodzi od razu -- to przypiecie istniejacego zachowania -- wiec zweryfikowany mutacyjnie (`objects` -> `global_objects`). Zlapana wlasna slaba asercja: `"articles" in tresc` na changeliscie bylo falszywie zielone, bo ten lancuch pojawia sie tez w bocznym `list_filter`. Przechodzilo nawet po wyrzuceniu kolumny z `list_display`. Asercje ida teraz po `class="field-<nazwa>"`, czyli po komorkach tabeli. Refs #742 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh
Faza 03 soft-delete: audyt kategorii B — miejsca, które MUSZĄ widzieć rekordy w koszu.
Stackowany na
feat/soft-delete(zawiera fazy 01 i 02, PR #741 scalony).Co to naprawdę domyka
Re-import z PBN publikacji, której rekord BPP siedzi w koszu, wywalał się
IntegrityError-em:Kosz jest bowiem jednocześnie:
Rekordto widok odfiltrowany podeleted_at,więc
rekord_w_bppzwracaNone;pbn_uidtoOneToOneField(unique=True)bezwarunku partial, więc rekord w koszu nadal to pole trzyma.
Import wchodził w gałąź „utwórz nowy" i wywracał cały przebieg. Teraz rekord
wraca z kosza, a fakt ląduje w rejestrze.
❗ Czego ten PR NIE naprawia (świadomie)
Poprzedni opis twierdził, że „blocker duplikatów" jest domknięty. Nie jest —
i po zmianie kierunku (niżej) nie ma być.
pbn_uid(pracawprowadzona ręcznie), skasowany miękko, nie zostanie znaleziony przez
matchuj_publikacje, bo ten idzie poRekord. Import utworzy nowy rekord.Po decyzji właściciela to zachowanie zamierzone: soft-delete znaczy, że
rekordu nie ma.
pbn_uid— i tam problemem nie byłcichy duplikat, tylko wywalenie importu.
Dwie zmiany kierunku — do świadomej akceptacji
1. Decyzja #14 planu: „POMIŃ + ZARAPORTUJ" → „PRZYWRÓĆ + ODNOTUJ".
Właściciel: PBN jest źródłem prawdy dla tych publikacji, więc skoro rekord tam
jest, ma wrócić także do BPP. Zastrzeżenie z pierwotnej decyzji nie zostało
uznane za nieważne, tylko przeniesione z „nie róbmy tego" na „róbmy, ale
zostawmy ślad". Śladem jest
pbn_integrator.RekordPrzywroconyPrzezImport.Wpis powstaje wyłącznie przy realnym wskrzeszeniu — zwykły re-import żywego
rekordu nie zostawia nic, inaczej rejestr zapełniłby się szumem (przypięte
osobnym testem).
2. Akcesory NIE zaglądają do kosza (cofnięcie pierwszej implementacji).
Pierwsza wersja fazy 03 kazała
rekord_w_bpp/get_bpp_publication/matchuj_publikacjezwracać rekordy skasowane. Właściciel rozstrzygnął:To nie była kosmetyka.
PublicationAdminwoła na wyniku.original— atrybutistniejący WYŁĄCZNIE na
Rekord— więc zwracanie modelu konkretnego wywalałoAttributeErrorna całej changeliście, przy zerowym pokryciu testami. Tosamo pominięcie w szablonie (
change_form.html) nie dawało wyjątku, tylko pustyhref. Dołożonesrc/pbn_api/tests/test_admin_rekord_w_bpp.py.Drugi skutek uboczny:
matchuj_publikacjema dwóch wołaczy o sprzecznychpotrzebach —
pbn_apiideduplikator_publikacji. Przełączenie go „na globalnymenedżer" sprawiło, że dedup zaczął podsuwać rekordy z kosza, wbrew decyzji
z Taska 8.
Zmiany w kodzie
pbn_integrator/kosz.pyprzywroc_jesli_w_koszu(),wskrzes_z_kosza_po_pbn_uid(),znajdz_lub_wskrzes_rekord()pbn_integrator/importer/{articles,books,chapters}.pyret is None(wcześniej wskrzeszanie siedziało w gałęziis not None, gdzie było no-opem)pbn_integrator/importer/chapters.pyznajdz_ksiazke_nadrzedna()— wydzielony i testowalnypbn_import/utils/publication_import.pyglobal_objects+hard_delete()przy czyszczeniu przed re-importembpp/models/praca_habilitacyjna.pyautor:OneToOneField→ForeignKey+ warunkowyphab_uniq_autor_zywy(migracjabpp/0500)bpp/admin/praca_habilitacyjna.pyclean_autor()— jawna walidacja „jeden autor, jedna żywa habilitacja"bpp/views/api/__init__.py,bpp/templates/browse/autor.htmlpbn_integrator/admin.py,models.py(content_type, object_id)(migracjapbn_integrator/0002)deduplikator_autorow/utils/merge.pyglobal_objects;KonfliktScalania; guard_w_koszu()przed kolejką PBNNa co zwrócić uwagę w recenzji
1. Zmiana typu pola
Praca_Habilitacyjna.autornaForeignKey(BLOKER 3z self-review). Twarde
UNIQUE (autor_id)nie zna kosza, więc scalanie autorów— które przenosi wiersze RAZEM Z KOSZEM, żeby nie zostawiać sierot — padało,
gdy duplikat miał habilitację w koszu, a główny żywą. Django wymusza
unique=TruewOneToOneField.__init__, więc warunkowy unique wymaga zmianytypu. Konsekwencje sprawdzone po kolei:
autor.praca_habilitacyjna_set(manager, nie obiekt);related_query_namedomyślnie = nazwa modelu, tak samo jak dla
praca_doktorska, który od zawszejest FK);
i oba zaświeciły się na czerwono;
OneToOneszło przez_base_manageri pokazywało rekordy z kosza; manager relacji FK już nie.
2. Walidacja unikalności w adminie zmienia mechanizm.
validate_unique()zgłasza błąd;
validate_constraints()(dlaMeta.constraints) cicho pomijaograniczenie, którego pole warunku (
deleted_at) jest wykluczone z walidacji —a
_get_validation_exclusions()wyklucza wszystko spozaMeta.fields, któreadmin nadpisuje spłaszczonymi
fieldsets. Sztuczka z ukrytym polem(
bpp/admin/core.py) działa dla inline'ów; tutaj wyprodukowałaby widoczny pustywiersz „Deleted at", więc jest jawne
clean_autor(). Constraint zostajeostateczną gwarancją w bazie.
3. Kosz przestał być drzwiami jednokierunkowymi. Detekcja kolizji autorstw
szła po menedżerze żywych, więc gdy publikacja była w koszu, kolizji nie było
widać: w koszu lądowały DWA wiersze
(rekord, glowny, typ), apublikacja.restore()wywalał się nawc_autor_uniq_rekord_autor_typ. Testkończy się jawnym
restore()jako dowodem.4.
Patentnie ma polapbn_uid— sprawdzone na modelach. Dlategowskrzes_z_kosza_po_pbn_uid()przyjmuje model jako parametr, zamiastiterować po wszystkich modelach publikacji (
.filter(pbn_uid_id=…)naPatentwywala
FieldError, bo Django resolwuje nazwy pól natychmiast).5. Zachowana historyczna różnica między
get_bpp_publicationarekord_w_bpp: przy wielu trafieniach pierwsza zwracaNone, druga sklejonetytuły; przy braku trafień druga spada do fuzzy matchingu. Nie ujednolicane przy
okazji — i dlatego helpery na ścieżce importu muszą przyjmować wejście
nie-modelowe (STRING/
None) bez wyjątku.Weryfikacja
Pełne
make testslokalnie, wszystkie trzy kroki (target przerywa się napierwszym błędnym, więc każdy odpalony i przeczytany osobno):
tests-without-playwrighttests-only-playwrightjs-tests(vitest)3 walidacja admina habilitacji), wszystkie pisane czerwień-najpierw
i zweryfikowane mutacyjnie — łącznie 11 mutacji, każda wywróciła
dokładnie ten test, który miała wywrócić
makemigrations --checkczysty dlabpp,pbn_integrator,pbn_apipre-commitzielony na 16 plikach fazy (w tymdjLinti hookDjango {#)Testsmapull_request: branches: [dev], a ten PR celuje wfeat/soft-delete.Jedyny zielony check to GitGuardian — to nie jest dowód, że testy
przeszły. Powyższe liczby pochodzą z przebiegu lokalnego. Testy na CI
odpalą się dopiero dla PR-a
feat/soft-delete→dev.make tests-without-playwrightzwraca EXIT 0 mimo porażek. Czytaćpodsumowanie pytest, nie kod wyjścia.
baseline-sql/) jest nieświeży — stoi nabpp/0487, gałąź nabpp/0500. Był taki już przed tym PR-em (fazy 01–02 dołożyły0488–0499).Zgodnie z CLAUDE.md odświeżenie robi się raz, przy scalaniu, a nie
w równoległych feature-branchach.
Dług przekazany dalej
docs/superpowers/HANDOFF-soft-delete-faza-04.md. Najważniejsze:scal_autorapolega dziś na TWARDEJ kaskadzieautor_duplikat.delete().Gdy oba kolidujące autorstwa są w koszu, wiersz duplikatu znika tylko dlatego,
że usunięcie autora kaskaduje po FK fizycznie. Faza 04 zmienia kasowanie
autora na miękkie — wtedy ten wiersz przetrwa jako sierota i zepsuje
restore(). Opisane z proponowaną poprawką.force=Trueomija wskrzeszenie, aleznajdz_ksiazke_nadrzednależy ZA tymguardem → przy
forceksiążka-matka jest wskrzeszana, a rozdział nie.hard_delete()na querysecie nie emitujepost_hard_delete(dla fazy 06).xfail(strict=True); PR upstreamsoynatan/django-easy-audit#348.
🤖 Generated with Claude Code
https://claude.ai/code/session_01G4vnLWzPinqUrj5GTjPRnh