Skip to content

soft-delete fazy 06+07: SoftDeleteLog z atrybucją + kosz w adminie - #792

Open
mpasternak wants to merge 18 commits into
feat/soft-delete-05bfrom
feat/soft-delete-06
Open

soft-delete fazy 06+07: SoftDeleteLog z atrybucją + kosz w adminie#792
mpasternak wants to merge 18 commits into
feat/soft-delete-05bfrom
feat/soft-delete-06

Conversation

@mpasternak

Copy link
Copy Markdown
Member

Stos: feat/soft-delete-06feat/soft-delete-05b (→ #767#755#745#312dev).

17 commitów: faza 06 (SoftDeleteLog, receivery, atrybucja użytkownika) i faza 07 (kosz w adminie). Żadna z nich nie była wcześniej wypchnięta.

Faza 06 — audyt „kto / dlaczego"

  • SoftDeleteLog (GFK + akcja/user/powod + pbn_queue_entry/pbn_status), migracja bpp/0503.
  • soft_delete_context(user=, reason=) — thread-local, bo sygnały pakietu niosą tylko sender/instance. Pominięty argument dziedziczy, nie zeruje.
  • Trzy receivery podpięte bez sender= → obejmują każdy model soft-delete, także znalezione dopiero asercją Zgloszenie_Publikacji.
  • Kosz przestaje liczyć się do ewaluacji (Cache_Punktacja_* sprzątane w receiverze).
  • hard_delete() na querysecie emituje sygnały (per instancja).

Faza 07 — kosz w adminie (5 publikacji + Autor)

BppSoftDeleteAdminMixin w src/bpp/admin/helpers/mixins.py, bez migracji:

  • changelist nad global_objects + filtr „Kosz" (?is_deleted=true = kosz, =all = wszystko, brak = tylko żywe),
  • JEDEN punkt wstrzyknięcia request.user (_soft_delete_user_context) — także dla hard_delete(), które user=/reason= nie przyjmuje,
  • akcje: „🗑️ Usuń do kosza (z powodem)" (strona pośrednia → SoftDeleteLog.powod), „♻️ Przywróć", „❌ Usuń TRWALE" (superuser-only, tylko z kosza),
  • guard autora-z-pracami pokazuje komunikat z wyjątku zamiast 500,
  • szwy pod przyszłe django-reversion (set_user, recover w get_urls).

Odstępstwa od planu fazy 07 (plan rozjechał się z kodem)

  1. Plan pinował nieistniejące set/get/clear_soft_delete_user — użyto soft_delete_context (co overview nakazywał explicite).
  2. hard_delete() nie przyjmuje user=/reason= — atrybucja wyłącznie przez kontekst.
  3. ⚠️ Plan kazał wpiąć mixin jako PIERWSZY. Jego get_queryset nie może wołać super(), więc z tej pozycji ucinał SiteFilteredAdminMixin w AutorAdmin — personel uczelni A widziałby i kasował autorów uczelni B. Mixin wpięty jako OSTATNI; strażnikiem jest test_mixin_nie_znosi_zawezenia_do_uczelni_w_autoradmin.
  4. Plan odwracał semantykę is_deleted na podstawie błędnego odczytu pakietu — zostawiono semantykę pakietową, żeby parametr w URL-u nie kłamał.

Szczegóły i uzasadnienia: docs/superpowers/HANDOFF-soft-delete-faza-08.md.

Weryfikacja

Przebieg Wynik
suita fazy 07 21 passed
test_soft_delete/ 197 passed, 1 xfailed
pytest -k admin (z Playwrightem) 845 passed
make tests-without-playwright 9669 passed, 1 failed, 4 skipped, 2 xfailed

Jedyna porażka to test_0499_odwracalnaTimeout (>90s) pod -n auto, nie asercja. Serialnie 32,98 s (faza 06 mierzyła 31,88/32,33 s); faza 07 nie dokłada migracji, a to test odwracalności migracji 0499.

⚠️ Baseline (baseline-sql/) nadal na bpp/0487 — odświeżenie raz, przy scalaniu całego feat/soft-delete do dev.
⚠️ CI nie uruchamia się na PR-ach do gałęzi feat/soft-delete*.

🤖 Generated with Claude Code

https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK

mpasternak and others added 18 commits August 16, 2026 22:38
Sygnaly pakietu django-soft-delete niosa wylacznie sender+instance, wiec
user i powod musza dojechac do receiverow innym kanalem. Thread-local
ustawiany przez delete(user=, reason=) jest najwezszym rozwiazaniem, ktore
obsluguje wszystkie modele soft-delete naraz i nie wymaga forka pakietu.

Sprzatanie w finally, nie po yield: guard fazy 04 przerywa delete() autora
z pracami przez ProtectedError, a przeciekly kontekst przypisalby cudzego
usera nastepnej operacji w tym samym watku (kolejny request na tym samym
workerze). Blad bylby cichy i nie do wykrycia po fakcie -- stad osobny test.

Reentrancja jest wymagana, nie kosmetyczna: waska kaskada fazy 02 wchodzi
w kontekst ponownie dla kazdego wiersza *_Autor, wiec bez odtworzenia
poprzednich wartosci kaskada wyzerowalaby usera w polowie operacji.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
Osobny model, a nie django-easy-audit, bo easy-audit zapisuje ZMIANY POL:
soft-delete widzi jako "deleted_at: null -> 2026-08-16", bez powodu, bez
zwiazku ze zleceniem wycofania z PBN i bez odpowiedzi na "co jeszcze poszlo
do kosza ta sama decyzja". SoftDeleteLog odpowiada na pytania operacyjne.

GFK, nie FK, bo log musi przezyc rekord: przy HARD_DELETE wiersz publikacji
znika fizycznie, a wpis zostaje jedynym sladem, ze istniala. CASCADE
skasowalby dowod razem z rekordem, PROTECT zablokowalby samo kasowanie.
Oba warunki maja test.

Dwa odejscia od litery planu, obie w miejscu, gdzie plan sam sobie
przeczyl albo lamal lokalna konwencje:

- content_type ma db_index=False -- osobny indeks jest redundantny wobec
  Meta.indexes Index(content_type, object_id), gdzie content_type jest
  kolumna wiodaca. Wzorzec z sasiedniego OplatyPublikacjiLog.
- Meta.indexes NIE dubluje indeksu na timestamp, ktory pole ma juz przez
  db_index=True (plan mial oba). Drugi identyczny indeks to czysty koszt
  zapisu, ponoszony przy KAZDYM wpisie -- takze dla kazdego wiersza
  *_Autor kasowanego kaskada.

Sygnatury pol PINNED zachowane co do joty. AUTH_USER_MODEL przez
settings.AUTH_USER_MODEL (konwencja repo), nie przez import modulu
settingsow, jak sugerowal plan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
Receivery podpiete BEZ sender=, wiec obejmuja kazdy model soft-delete;
rejestracja w BppConfig.ready() z dispatch_uid (ready() bywa wolane
wielokrotnie, m.in. przy TransactionTestCase -- bez uid-a kazdy soft-delete
produkowalby dwa wpisy logu).

PK PRZY HARD-DELETE. Plan kazal ustalic to empirycznie, nie zgadywac --
ustalone testem-sonda: SoftDeleteModel.hard_delete() wola Model.delete(),
kolektor Django zeruje pk na koniec, a post_hard_delete leci PO tym. Receiver
dostaje wiec instancje z pk=None. Naiwny zapis dalby object_id=None, czyli
IntegrityError W RECEIVERZE -- audyt przewrocilby operacje, ktora ma tylko
obserwowac. Stad BppPkPrzedHardDeleteMixin zapamietujacy pk przed
skasowaniem; pk_dla_audytu() czyta go z fallbackiem i rzuca opisowy
RuntimeError, gdy modelowi brakuje mixinu.

Mixin jest zwykla klasa (nie models.Model), wiec dopisanie go do baz
istniejacych modeli NIE generuje migracji -- zweryfikowane
makemigrations --check.

Test-sonda na kolejnosc zerowania pk zostaje jako straznik: gdy pakiet
kiedys ja zmieni, zapali sie tam, a nie w postaci logow z zapasowego pola.

STRAZNIK KOMPLETNOSCI ZNALAZL SZOSTY MODEL. Testowa asercja "kazdy
SoftDeleteModel ma mixin" wykryla zglos_publikacje.Zgloszenie_Publikacji --
model soft-delete NIEZALEZNY od faz 01-04, uzywajacy pakietu od dawna na
wlasne potrzeby. Nie bylo go na zadnej liscie w planie ani w handoffie. Bez
mixinu jego hard_delete() wywalilby sie dopiero na produkcji. To dokladnie
powtorka lekcji z handoffu 4.2 (faza 04 pominela czwartego dziedzica
abstraktu): wyliczanka modelow z glowy jest niepelna, asercja nad
apps.get_models() nie jest.

Skutek uboczny do odnotowania: operacje na Zgloszenie_Publikacji trafiaja
teraz do SoftDeleteLog. Bez skutkow PBN -- gate kolejkowania (Task 6) obejmie
wylacznie 5 modeli publikacji.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
Fazy 02/04 przyjmowaly user=/reason= i je PORZUCALY ("konsumuje je
SoftDeleteLog z fazy 06"). Plan zakladal, ze owijaja super().delete()
w soft_delete_context -- w rzeczywistosci nie wolaja super().delete() wcale
(swiadomie, zeby uniknac refleksyjnej kaskady pakietu), tylko same ustawiaja
deleted_at i same wysylaja sygnal. Faza 06 domyka wiec obietnice: kontekst
zakladany jest w 4 metodach (publikacje delete/restore, Autor
delete/restore). Bez tego mechanizm atrybucji dzialalby wylacznie dla
wolajacych, ktorzy sami weszliby w context manager -- czyli dla nikogo.

Kontekst obejmuje CALE cialo delete(), nie samo post_soft_delete.send():
kaskada na *_Autor wysyla wlasne sygnaly, ktore maja byc zalogowane z tym
samym userem i powodem.

POMINIETY ARGUMENT DZIEDZICZY, NIE ZERUJE -- regula wymuszona przez test.
Plan przewidywal dwa sposoby atrybucji, ktore po wpieciu okazaly sie
wzajemnie sprzeczne: delete() zaklada kontekst ZAWSZE, wiec wywolanie bez
user= wewnatrz jawnego soft_delete_context(user=X) zerowalo X i operacja
trafiala do logu jako niczyja (test z planu padal na assert None == user).
Regula siedzi w samym context managerze, nie w czterech miejscach wywolan --
inaczej nastepny model soft-delete zgubilby usera po cichu. Uzasadnienie:
opakowanie, ktore nie wnosi informacji, nie ma prawa jej niszczyc; None
znaczy "nie wiem", a nie "wiem, ze nikt".

Log jest wierny sygnalom: soft-delete publikacji z N autorami daje 1+N
wpisow (decyzja wlasciciela). Test pilnuje, ze wiersze *_Autor dziedzicza
usera i powod przez reentrancje.

156 passed w src/bpp/tests/test_soft_delete/ po wpieciu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
Task 5 planu POMINIETY zgodnie z handoffem -- zakolejkuj_wycofanie/wysylke
juz istnieja z fazy 05a, a shim z planu tworzyl wpisy golym objects.create(),
wiec omijalby sprobuj_utowrzyc_wpis (TOCTOU, uczelnia, operacja).

GATE PO TYPIE, NIE PO pbn_uid -- plan opieral sie tu na nieprawdzie.
Twierdzil (jako fakt zweryfikowany), ze "Autor i *_Autor nie maja pbn_uid",
i proponowal uniwersalny getattr(instance, "pbn_uid_id", None). Autor.pbn_uid
ISTNIEJE (FK do pbn_api.Scientist, autor.py), wiec ten gate wstawilby AUTORA
do kolejki eksportu PUBLIKACJI i odpalil dla niego wysylke do PBN. Ma na to
test.

Sam pbn_uid nie wystarczylby takze od drugiej strony: RESTORE wola
zakolejkuj_wysylke, ktora swiadomie NIE MA gate'u na pbn_uid (faza 05a),
wiec przyjelaby kazdy wiersz *_Autor przywracany kaskada -- publikacja
z N autorami dawalaby N zbednych zlecen. Gate isinstance(
BppPublikacjaSoftDeleteMixin) zamyka oba przypadki naraz; test liczy wpisy.

UCZELNIA WYPROWADZANA Z REKORDU (decyzja wlasciciela). Receiver nie ma
requestu, z ktorego wszyscy pozostali wolajacy biora tenanta
(Uczelnia.objects.get_for_request), a bez uczelni _pozyskaj_klienta_pbn()
spada na "jedyna-albo-glosny-blad" i w multi-hosted wycofanie konczy sie
FINISHED_ERROR. uczelnia_rekordu() ODWRACA regule przynaleznosci z fazy 05b
(naleza_wydawnictwa / naleza_prace), zamiast definiowac druga, konkurencyjna;
odwracamy zamiast wolac wprost, bo cerif_export zalezy od bpp, nie odwrotnie.

global_objects w tym odczycie jest KONIECZNE, nie ostrozonosciowe: waska
kaskada fazy 02 kasuje wiersze *_Autor PRZED wyslaniem post_soft_delete
rodzica, wiec objects zwrocilby pustke i uczelnia wychodzilaby None przy
KAZDYM kasowaniu. Potwierdzone mutacja: podmiana na objects wywala dokladnie
dwa testy, ktore tego pilnuja.

Dwuznacznosc (praca wspolautorska miedzy uczelniami) -> None, nie zgadywanie:
"pierwsza z brzegu" wyslalaby wycofanie przez konto PBN cudzego tenanta.

Odnotowane ograniczenie: pbn_status="" nie odroznia "nie bylo czego
kolejkowac" od "pominieto, bo rekord juz czeka w kolejce" -- obie funkcje
kolejkujace zwracaja None i receiver nie ma ich jak rozroznic. Udokumentowane
testem test_restore_przy_niezakonczonym_wycofaniu_nie_dubluje_wpisu.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
Cache_Punktacja_Autora i Cache_Punktacja_Dyscypliny NIE MAJA FK do
publikacji -- kluczem jest tablica rekord_id = [content_type_id, pk]. Nie
rusza ich wiec ani kaskada Django, ani waska kaskada *_Autor z fazy 02, ani
triggery denormalizacji. Bez tego soft-deletowana praca nadal wnosila sloty
i punkty do ewaluacji (luka nieobjeta zadnym innym mechanizmem, spec 2.5b).

GATE WEZSZY NIZ W PLANIE, I TO JEST POPRAWKA FAKTOGRAFICZNA. Plan mowil
"tylko 5 modeli publikacji". przelicz_punkty_dyscyplin() maja TRZY:
Wydawnictwo_Ciagle, Wydawnictwo_Zwarte, Patent -- tylko one dziedzicza
ModelZPrzeliczaniemDyscyplin. Prace dyplomowe nie maja nawet tej metody, wiec
gate "5 modeli" wywalilby sie na AttributeError przy kasowaniu doktoratu.
Gate przez isinstance(ModelZPrzeliczaniemDyscyplin) jest jednoczesnie
odpowiedzia na oba ostrzezenia planu: zawezenie po typie nadawcy ORAZ
gwarancja, ze kaskada *_Autor nie wyzwoli przeliczenia drugi raz (te wiersze
tego abstraktu nie dziedzicza). Test liczy wywolania removeEntries: dokladnie
jedno mimo 1+N sygnalow.

RESTORE przelicza, a nie odtwarza z kopii: przez czas pobytu w koszu mogly
sie zmienic dyscypliny autorow albo progi punktowe, wiec odtworzenie starych
wartosci przywrociloby nieaktualny stan.

POMIAR (krok 6b.3 planu): restore() z przeliczeniem = 46.7 ms dla rekordu
z 2 autorami. Ponizej progu 1 s, wiec bez eskalacji do fazy 07 -- ale przy
masowym przywracaniu koszt jest liniowy (100 rekordow ~ 4.7 s w jednym
zadaniu HTTP), co odnotowuje handoff.

Fixture bez_celery przeniesiony do conftest.py katalogu: testy biegna
z CELERY_TASK_ALWAYS_EAGER, wiec bez atrapy zakolejkowanie wykonywaloby
realna wysylke do PBN w srodku delete()/restore().

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
…ejestracji

TASK 8 (z handoffu 7, plan nie mial na to taska -- decyzja wlasciciela).
Pakietowy hard_delete() na querysecie to goly super().delete()
(django_softdelete/managers.py): jedno zapytanie bulk, ZERO sygnalow. Rekordy
znikaly fizycznie bez jednego wpisu w SoftDeleteLog -- dokladnie ta klasa
cichej utraty, przed ktora ten log ma chronic, i to na najbardziej
prawdopodobnej drodze masowego kasowania (oproznianie kosza z admina fazy 07).

Nadpisane w BppSoftDeleteQuerySet (objects + global_objects)
i BppDeletedQuerySet (deleted_objects) -- razem pokrywaja wszystkie trzy
managery BPP. Iteracja per instancja, wiec kazdy wiersz przechodzi przez
BppPkPrzedHardDeleteMixin.hard_delete() i wysyla post_hard_delete.

Koszt: N zapytan zamiast jednego. Swiadomy i zgodny z zasada, ktora rzadzi
juz gate'em update() i waska kaskada fazy 02: operacja masowa nie ma prawa
byc tansza kosztem pominiecia sygnalow. Pakiet stosuje ja zreszta sam --
jego SoftDeleteQuerySet.delete() rowniez iteruje po instancjach. Zwrotka
zachowuje kontrakt Django (liczba, {etykieta: liczba}), co ma osobny test.

TASK 7: dwa straznikami regresji. Pierwszy dowodzi, ze receivery sa podpiete
przez BppConfig.ready(), a nie przez przypadkowy import -- bez niego cala
reszta testow przechodzilaby takze wtedy, gdyby w produkcji nikt register()
nie wolal. Drugi pilnuje idempotencji: ready() bywa wolane wielokrotnie,
a bez dispatch_uid kazdy soft-delete tworzylby dwa identyczne wpisy, czyli
audyt zaczalby zmyslac.

Testy Taska 8 siegaja po _wydawnictwo_ciagle_maker wprost, bo fixture-factory
wydawnictwo_ciagle_maker w fixtures/conftest_publications.py:81 nie ma
dekoratora @pytest.fixture i pytest jej nie rejestruje (zastany martwy kod,
nie ruszany).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
Handoff zbiera cztery rozjazdy planu z kodem (Autor.pbn_uid, brak
super().delete() w fazach 02/04, sprzecznosc dwoch sposobow atrybucji, gate
punktacji na trzech a nie pieciu modelach), szosty model soft-delete znaleziony
przez asercje nad apps.get_models(), pomiar kosztu restore() oraz wskazowki
wprost dla admina fazy 07.

Docstring receiverow wymienia teraz wszystkie objete modele -- z adnotacja,
zeby tej listy NIE powielac w kodzie: zrodlem prawdy jest asercja testowa,
bo wyliczanka z glowy juz dwukrotnie okazala sie niepelna.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
make tests-without-playwright: 9647 passed, 2 failed. Obie porazki to
Timeout (>90 s), nie asercje, i NIE sa regresja fazy 06 -- czasy seryjne
zmierzone na obu galeziach sa identyczne co do szumu (45.70/31.88 s na
bazie wobec 43.16/32.33 s na fazie 06). Zaden z tych testow nie wykonuje
operacji soft-delete, wiec receivery nie maja sie w nich gdzie odpalic;
wklad fazy 06 do grafu migracji to jedno CreateModel.

Oba testy maja ~2x zapasu do limitu 90 s, a -n auto odpala 10 workerow --
na wspoldzielonym hoscie to za malo. Ta sama klasa problemu co handoff
fazy 05a 8. Kandydat do podniesienia timeoutu, poza zakresem fazy 06.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
BppSoftDeleteAdminMixin podmienia manager bazowy changelisty na
global_objects, zeby dalo sie otworzyc i przywrocic rekord z kosza.
Kosz chowa PokazSkasowaneFilter — brak parametru = tylko zywe (pakietowy
SoftDeleteFilter pokazywalby wszystko, a po poszerzeniu querysetu nie ma
juz nikogo innego, kto by kosz schowal). Semantyka wartosci zostaje
pakietowa: is_deleted=true to rekordy skasowane.

Mixin wpinany jako OSTATNI przed admin.ModelAdmin, wbrew planowi fazy 07
(kazal "PIERWSZY"). get_queryset nie moze wolac super() — musi podmienic
manager — wiec na poczatku MRO uciolby caly lancuch, w tym
SiteFilteredAdminMixin.get_queryset w AutorAdmin (zawezenie do wlasnej
uczelni, FD#390).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
…kosz

_soft_delete_user_context deleguje do soft_delete_context z fazy 06 i jest
jedynym punktem, w ktory wchodzi request.user — takze dla hard_delete(),
ktore (wbrew planowi fazy 07) NIE przyjmuje user=/reason= i innego kanalu
atrybucji nie ma.

Bez tego rekord i tak ladowal w koszu (delete() modelu jest miekkie), ale
wpis SoftDeleteLog powstawal z user=None.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
Akcja dziala na querysecie changelisty (juz zawezonym przez filtry i przez
SiteFilteredAdminMixin), a nie na swiezym deleted_objects — dociaganie po
pk omijaloby zawezenie do wlasnej uczelni. Konsekwencja: przywracanie
wymaga filtra "Tylko skasowane"; puste zaznaczenie tlumaczy komunikat
zamiast cichego "przywrocono: 0".

Restore pomija rekordy zywe — restore() publikacji przelicza punktacje
i kolejkuje wysylke do PBN, wiec no-op nie bylby darmowy.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
hard_delete() nie przyjmuje user=/reason= (plan fazy 07 zakladal inaczej),
wiec atrybucje niesie wylacznie kontekst z _soft_delete_user_context.

Akcja kasuje WYLACZNIE rekordy z kosza. Cache_Punktacja_* nie ma FK do
publikacji (klucz to tablica [content_type_id, pk]), wiec sprzata ja
dopiero receiver post_soft_delete — twarde skasowanie rekordu zywego
zostawiloby punktacje wskazujaca na nieistniejacy rekord i wciaz liczaca
sie do ewaluacji.

Fixture staff_user nalezy do grupy "wprowadzanie danych": staff BEZ grupy
wywraca render menu admina 500-tka (menu.py:302 robi bezwarunkowe
del menu.children[-1].children[-1] na pustej liscie) — bug niezalezny od
soft-delete, odnotowany w handoffie.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
…owod

Strona posrednia pyta o powod, bo strona potwierdzenia Django jest
zbudowana wokol kolektora (co zniknie), a nie wokol metadanych operacji.
delete_selected zostaje dzialajace — ta sama operacja, tylko bez
uzasadnienia.

Potwierdzenie przenosi dalej select_across i index: przy zaznaczeniu
"wszystkie pasujace" Django ignoruje _selected_action i bierze caly
queryset, wiec bez tego czesc rekordow po cichu nie trafilaby do kosza.
Podglad listy jest przyciety do 50 pozycji (licznik z .count(), wiec
liczba pozostaje prawdziwa).

Test pilnuje tez, ze kaskada na *_Autor dziedziczy powod i usera rodzica.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
…sion

Wszedzie mixin wpiety jako OSTATNI przed baza terminalna, nie pierwszy.
test_mixin_nie_znosi_zawezenia_do_uczelni_w_autoradmin jest strazem tej
decyzji: przy wpieciu wg planu (PIERWSZY) get_queryset mixinu uciolby
lancuch razem z SiteFilteredAdminMixin i personel jednej uczelni
zobaczylby autorow drugiej.

get_urls zostawia szew pod przyszly recover z django-reversion — recover
wskrzesza rekord poza przeplywem soft-delete (bez WYSYLKA do PBN, bez
SoftDeleteLog, bez przeliczenia punktacji, z pominieciem warunkowego
unique na Autor.slug).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
usun_do_kosza to sciezka krytyczna: wlasna akcja nie przechodzi przez
kolektor Django, ktory dla delete_selected zatrzymalby operacje na FK
PROTECT — ProtectedError szedl prosto z Autor.delete() do 500.

Komunikat bierzemy Z WYJATKU, nie piszemy wlasnego: guard zna liczbe
i rodzaj powiazan i juz je opisuje po polsku, a drugi tekst rozjechalby
sie z oryginalem przy pierwszej zmianie listy relacji.

Lapanie jest per instancja — jeden zablokowany autor nie przewraca calego
zaznaczenia.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
…fazy 08

Staff moze do kosza (z powodem, z atrybucja), ale akcja "Usun trwale" nie
jest mu w ogole oferowana. delete_selected takze idzie przez nasz
delete_queryset, czyli do kosza.

Weryfikacja: 21 testow fazy, 197 w rodzinie soft-delete, 845 w regresji
adminow (z Playwrightem), 9669 w make tests-without-playwright. Jedyna
porazka to znany timeout test_0499_odwracalna pod -n auto — serialnie
32,98 s (faza 06 mierzyla 31,88/32,33 s), a faza 07 nie dokłada zadnej
migracji.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
Galaz feat/soft-delete-06 (fazy 06 i 07) byla jedynym ogniwem stosu bez
numeru PR — bo faza 06 nigdy nie zostala wypchnieta.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK
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