soft-delete fazy 06+07: SoftDeleteLog z atrybucją + kosz w adminie - #792
Open
mpasternak wants to merge 18 commits into
Open
soft-delete fazy 06+07: SoftDeleteLog z atrybucją + kosz w adminie#792mpasternak wants to merge 18 commits into
mpasternak wants to merge 18 commits into
Conversation
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
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stos:
feat/soft-delete-06→feat/soft-delete-05b(→ #767 → #755 → #745 → #312 →dev).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), migracjabpp/0503.soft_delete_context(user=, reason=)— thread-local, bo sygnały pakietu niosą tylkosender/instance. Pominięty argument dziedziczy, nie zeruje.sender=→ obejmują każdy model soft-delete, także znalezione dopiero asercjąZgloszenie_Publikacji.Cache_Punktacja_*sprzątane w receiverze).hard_delete()na querysecie emituje sygnały (per instancja).Faza 07 — kosz w adminie (5 publikacji +
Autor)BppSoftDeleteAdminMixinwsrc/bpp/admin/helpers/mixins.py, bez migracji:global_objects+ filtr „Kosz" (?is_deleted=true= kosz,=all= wszystko, brak = tylko żywe),request.user(_soft_delete_user_context) — także dlahard_delete(), któreuser=/reason=nie przyjmuje,SoftDeleteLog.powod), „♻️ Przywróć", „❌ Usuń TRWALE" (superuser-only, tylko z kosza),django-reversion(set_user, recover wget_urls).Odstępstwa od planu fazy 07 (plan rozjechał się z kodem)
set/get/clear_soft_delete_user— użytosoft_delete_context(co overview nakazywał explicite).hard_delete()nie przyjmujeuser=/reason=— atrybucja wyłącznie przez kontekst.get_querysetnie może wołaćsuper(), więc z tej pozycji ucinałSiteFilteredAdminMixinwAutorAdmin— personel uczelni A widziałby i kasował autorów uczelni B. Mixin wpięty jako OSTATNI; strażnikiem jesttest_mixin_nie_znosi_zawezenia_do_uczelni_w_autoradmin.is_deletedna 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
test_soft_delete/pytest -k admin(z Playwrightem)make tests-without-playwrightJedyna porażka to
test_0499_odwracalna—Timeout (>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 migracji0499.baseline-sql/) nadal nabpp/0487— odświeżenie raz, przy scalaniu całegofeat/soft-deletedodev.feat/soft-delete*.🤖 Generated with Claude Code
https://claude.ai/code/session_01Drz3jfnuP864JmYxqfjsYK