Aktualizacja do Django 6.1 - #731
Conversation
…ppelli Przygotowanie pod Django 6.1. django-prometheus 2.5.0 ma twardy cap `Django<6.1`. Integracja byla w calosci opcjonalna (flaga DJANGO_BPP_ENABLE_PROMETHEUS, domyslnie False), wiec usuwamy pakiet wraz z obsluga flagi, middleware, wpisem w INSTALLED_APPS i routingiem /metrics. Grappelli nie wymaga zmiany wersji — 5.0.0 jest juz linia dla Django 6.x. Dodany komentarz przy pinie, bo numeracja Grappelli jest przesunieta o jeden wstecz wzgledem Django (5.0.x -> Django 6.x, 4.0.x -> Django 5.x) i dependabot juz raz podbil to w zla strone (29f0ecd), zostawiajac skorke admina dla Django 6.x na Django 5.2. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F134BWb3YoYPQ6zjzqoqds
Podnosi Django z 5.2.16 LTS do 6.1 i `requires-python` do >=3.12 (wymaganie 6.1; obraz Dockera i glowne CI juz stoja na 3.12). Zaleznosci: * `django-redis-iplweb` -> upstreamowy `django-redis` 7.x. Fork byl czystym republishem (zero kodu iplweb, ten sam modul `django_redis`) i utknal na `Django<6.1`. Nazwa backendu w konfiguracji sie NIE zmienia, wiec wdrozenia nie wymagaja akcji. * `djangorestframework` i `django-pbn-client` TYMCZASOWO z gita — oba maja fix na glownej galezi, ale nie na PyPI. Warunki wyjscia opisane przy pinach w [tool.uv.sources]. Naprawione regresje 6.1 (kazda potwierdzona kontrola: przechodzi na 5.2, oblewa na 6.1 przy niezmienionym otoczeniu): * `ForwardManyToOneDescriptor.get_queryset()` wymaga teraz keyword-only `instance` (dokłada `.fetch_mode()` z jej stanu). Wolane bylo w czasie definicji klasy formularza — zastapione siegnieciem po `_base_manager` wprost, czyli tym, co deskryptor robil pod spodem. * Aliasy SELECT-a sa konsekwentnie cytowane, przez co odziedziczone `Meta.ordering` w zapytaniu zlozonym kompilowalo sie do `ORDER BY "col2"` -> ProgrammingError. Puste `.order_by()` na operandach `union()`, na jego wyniku i na wyniku `difference()`. * `BaseModelForm.validate_constraints()` to metoda NOWA w 6.1 (w 5.2 nie istniala). Psula podmiane kierownika projektu w jednym zapisie: wiersz oznaczony DELETE wciaz jest w bazie. Django koordynuje walidacje miedzy formularzami tylko dla unikalnosci; dla constraintow odpowiednika nie ma. * `Script.__str__` renderuje teraz przez staticfiles storage, wiec URL zawiera content-hash — asercja w tescie toleruje go zamiast porownywac literal. Dodatkowo: shim `django.utils.itercompat` dla `django-admin-tools` 0.9.3 (martwy import usunietego modulu), usuniete martwe `FORMS_URLFIELD_ASSUME_HTTPS` wraz z jego wyciszaczem w pytest.ini. Weryfikacja: 9308 zdanych, 0 porazek (bez Playwrighta). `manage.py check` czysto. Dryf `makemigrations --check` identyczny jak na 5.2 (stan zastany). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F134BWb3YoYPQ6zjzqoqds
pip-audit na PR-ze zglosil h2 4.3.0 (fix w 4.4.1). CVE dotyczy tak samo gałęzi `dev` — tam h2 rowniez jest 4.3.0 — zlapane przy okazji tego PR-a, bo `dev` nie mial przebiegu pip-audit od publikacji CVE. h2 przychodzi tranzytywnie przez twisted[http2]/daphne, wiec floor ladnie pasuje do istniejacej sekcji `constraint-dependencies`. Podnosi rowniez hpack 4.1.0 -> 4.2.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F134BWb3YoYPQ6zjzqoqds
PR iplweb/django-pbn-client#5 zostal zmergowany (2026-08-07 07:16Z), a 0.2.3 trafilo na PyPI (08:22Z) — z `django>=5.2` zamiast dawnego capu `django<5.3` i klasyfikatorem `Framework :: Django :: 6.1`. Wraca wiec z rejestru zamiast z galezi feature'owej. Floor podniesiony do 0.2.3, bo ponizej tej wersji Django 6.1 sie nie rozwiaze. Poza porzadkiem to takze zdjecie realnego ryzyka: pin wskazywal na commit NIEZMERGOWANEJ galezi, ktora po merge'u zwykle znika — build produkcyjny przestalby sie budowac. Dodatkowo pakiet wraca do zakresu `pip-audit`, ktory pomija zaleznosci z URL-i. Zostaje jeden tymczasowy pin git: djangorestframework (3.18.0 wciaz niewydane, PR encode/django-rest-framework#10015 otwarty). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F134BWb3YoYPQ6zjzqoqds
Gotowość do merge'u — co jest sprawdzone, a co nieCo merge do
|
* fix(admin): filtr "Wydział" na liście autorów listował 504 jednostki zamiast 7 wydziałów `AutorAdmin.list_filter` miał goły string "aktualna_jednostka__wydzial", z którego Django budowało `RelatedFieldListFilter`, a ten woła `field.get_choices()`. Po Fazie B (#438) denorm `Jednostka.wydzial` jest self-FK na `Jednostka`, więc `get_choices()` enumerowało CAŁĄ tabelę jednostek: na kopii bazy produkcyjnej 504 opcje w dropdownie zamiast 7 jednostek-korzeni ("wydziałów"), plus 504 zapytania na każdy request changelisty (każde `Jednostka.__str__` czyta `self.uczelnia`). Faza B naprawiła to `WydzialFilter`-em, ale tylko dla `JednostkaAdmin` — `AutorAdmin` został przeoczony. Dokładamy `WydzialAutoraFilter`: podklasę `WydzialFilter`, która dziedziczy bez zmian `lookups()` (tylko korzenie, zawężone do uczelni z requestu) i `has_output()` (bramka `uzywaj_wydzialow`), a nadpisuje jedynie `queryset()` — bo tu zawężamy `Autor`, więc do korzenia trzeba dojść przez `aktualna_jednostka__` (`Q(aktualna_jednostka__wydzial_id=v) | Q(aktualna_jednostka_id=v)`). Świadoma zmiana kontraktu URL: parametr filtra to teraz `?wydzial=<id>` zamiast `?aktualna_jednostka__wydzial__id__exact=<id>`. Filtry admina to ulotny stan UI, nie trwałe linki, więc to akceptujemy; stary querystring degraduje się do 400 (`DisallowedModelAdminLookup`, zmierzone testem), a nie do 500. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWWemKMkQMQmHZZPSmCdid * fix(admin): listy filtrow admina jednym zapytaniem zamiast N+1 Dwa niezalezne defekty w `bpp/admin/filters.py`, oba wykryte pomiarem na kopii bazy produkcyjnej (504 jednostki, 68 tys. autorow): 1. `JednostkaFilter.lookups()` mial `select_related("wydzial")`, ale `Jednostka.__str__` czyta DWA FK -- takze `self.uczelnia` (bramka `uzywaj_wydzialow`). Kazda pozycja listy kosztowala osobny SELECT: 504 zapytania na KAZDE wejscie na changeliste autorow. Cacheops zamienial je na trafienia w Redis (`bpp.uczelnia` jest w regulach), wiec licznik zapytan SQL tego nie pokazywal -- ale 504 round-tripy do Redisa nadal kosztowaly ~200 ms na request. 2. `LogEntryFilterBase.lookups()` zawezal queryset przez `.only("pk", "username")`, a `BppUser.__str__` czyta jeszcze `last_name` i `first_name`. Kazde pole odroczone to osobny `refresh_from_db()` per uzytkownik, czyli DWA dodatkowe SELECT-y na wiersz -- "optymalizacja", ktora kosztowala zamiast oszczedzac. Na produkcji 156 zapytan na wejscie na changeliste wydawnictw ciaglych, i w przeciwienstwie do slownikow `bpp.bppuser` NIE jest cache'owany przez cacheops, wiec szly wprost do PostgreSQL. Zmierzony efekt (kopia produkcji, produkcyjne reguly CACHEOPS): changelist wyd. ciaglych 190 -> 34 zapytania, changelist autorow 620 -> 452 ms (przy tej samej liczbie zapytan SQL -- zniknely round-tripy do Redisa). Testy pilnuja sedna: obie listy powstaja DOKLADNIE jednym zapytaniem, niezaleznie od liczby pozycji. Drugi test dodatkowo asertuje, ze imie i nazwisko sa w etykiecie -- inaczej `only()` znow by je pominelo, a `__str__` po cichu degradowalby do samego `username`. Uporzadkowana tez kolejnosc importow w `test_filters.py` (plik nie przechodzil `ruff check` juz przed ta zmiana; pre-commit sprawdza tylko pliki zmieniane, wiec nikt tego nie zauwazyl). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWWemKMkQMQmHZZPSmCdid * fix(browse): liczba autorow na indeksie jednostek jednym agregatem `browse/jednostki.html` wolal `item.aktualna_jednostka.count` -- relacja ODWROTNA (Autor -> Jednostka), wiec kazde uzycie to osobny `COUNT(*)`. Szablon uzywal go 3-4 razy na wiersz (warunek `> 0`, sama liczba, dwa warunki odmiany), a strona pokazuje domyslnie 150 jednostek. Na kopii bazy produkcyjnej dawalo to 514 zapytan na JEDNO wejscie na indeks jednostek -- najwiekszy pojedynczy koszt zapytan w calej czesci publicznej. Ani cacheops, ani fetch modes z Django 6.1 tego NIE lapaly: queryset dotyczy `bpp.autor`, ktorego nie ma w regulach CACHEOPS, a `FETCH_PEERS` obsluguje leniwe FK i pola odroczone -- nie `RelatedManager.count()`. Zmierzone: z globalnym FETCH_PEERS bylo 514 -> 514 zapytan, czyli zero zmiany. Lekarstwem jest adnotacja, nie tryb pobierania. Zmierzony efekt (kopia produkcji, produkcyjne reguly CACHEOPS): 514 -> 3 zapytania, 137 -> 20 ms. Adnotacja jest tu bezpieczna (nie zawyza liczb przez zdublowane wiersze), bo -- jak dokumentuje komentarz w `Browser.get_queryset` -- zadna sciezka filtrowania `JednostkiView` nie mnozy wierszy: filtr literki to `istartswith` na wlasnej kolumnie, fulltext dla `Jednostka` to predykat na jednokolumnowym tsvectorze bez JOIN-a, a `scope_jednostki_do_uczelni` porownuje skalarny FK. Sprawdzone tez wyczerpujaco: dla WSZYSTKICH 504 jednostek z bazy produkcyjnej adnotacja dala te same liczby, co `.count()` per wiersz (0 roznic, suma 60 711 autorow), a wyrenderowany HTML jest identyczny bajt w bajt. Dwa testy, bo zmiana ma dwa rozne ryzyka: * `test_JednostkiView_liczba_autorow_jednym_zapytaniem` -- sedno, czyli brak zapytania per wiersz, * `test_browse_jednostki_pokazuje_liczbe_autorow` -- literowka w nazwie adnotacji NIE wywalilaby wyjatku (Django renderuje nieistniejaca zmienna jako pusty lancuch), wiec strona po cichu przestalaby pokazywac liczby. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWWemKMkQMQmHZZPSmCdid * feat(admin): wlacz FETCH_PEERS (Django 6.1) dla querysetow adminow Django 6.1 wprowadzilo *fetch modes*. `FETCH_PEERS` sprawia, ze PIERWSZE leniwe dotkniecie relacji (albo pola odroczonego) na obiekcie z querysetu dociaga ja HURTEM dla calego rodzenstwa z tego samego pobrania (`prefetch_related_objects` pod spodem) -- N+1 zamienia sie w 2 zapytania, bez zgadywania z gory, ktore FK dotknie szablon. Changelisty admina to najgestsze w BPP skupisko tego wzorca: `list_display` i `__str__` modeli siegaja po FK, ktorych nikt nie zadeklarowal w `list_select_related`. Ustawiamy tryb w `BaseBppAdminMixin.get_queryset`, czyli w jednym miejscu dla 31 adminow. Zmierzone na kopii bazy produkcyjnej (produkcyjne reguly CACHEOPS), zapytania i mediana czasu na request: changelist jednostek 66 -> 17 zapytan, 185 -> 120 ms changelist wyd. zwartych 220 -> 40 zapytan, 227 -> 184 ms changelist wyd. ciaglych 190 -> 38 zapytan, 221 -> 190 ms Dlaczego TU, a nie globalnie (podstawienie `DEFAULT_FETCH_MODE`): `track_peers` trzyma `weakref` do kazdej instancji z pobrania, wiec koszt ponosilby KAZDY queryset w aplikacji, a zysk jest skoncentrowany w adminie. Samo `get_queryset` wystarcza, bo `QuerySet._clone()` przenosi `_fetch_mode` -- tryb przezywa filtry, sortowanie i slicing dokladane przez dalsze mixiny i przez sam `ChangeList`. Test pilnuje obu tych wlasnosci osobno. Semantyka sie NIE zmienia: `fetch_one` i `fetch_many` ida ta sama sciezka managera (`_base_manager`) -- tryb nie zaczyna nagle odfiltrowywac rekordow, co jest istotne przy soft-delete. Sprawdzone tez empirycznie na danych produkcyjnych: 15 stron (publiczne, admin, API) zwrocilo wynik identyczny bajt w bajt w obu trybach. WYMAGA Django >= 6.1, dlatego ta gałąź celuje w `django-6.1`, a nie w `dev`. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XWWemKMkQMQmHZZPSmCdid --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Audyt wydajnosci pod Django 6.1 zostal zrobiony na kopii bazy produkcyjnej
przy pomocy harnessu, ktory dotad zyl jako nietrackowane pliki na jednej
maszynie. To czynilo wyniki nieodtwarzalnymi, a wnioski niesprawdzalnymi --
wiec harness wchodzi do repo razem z dokumentacja.
`bench/` (4 skrypty):
* `bench_orm.py` -- `--inwentarz` (detekcja N+1 przez wlasny FetchMode,
ktory liczy leniwe pobrania ze stosem wywolan)
oraz `--pomiar [--tryb peers]`,
* `bench_rownowaznosc.py` -- dowod, ze FETCH_PEERS nie zmienia tego, co
strona zwraca (porownanie bajt w bajt, po
wyzerowaniu tokenow CSRF i znacznika czasu),
* `bench_atrybucja.py` -- rozbicie pobran JEDNEGO pola na miejsca
wywolania (tym rozwiazalem rozbieznosc 1058 pobran
przy 504 jednostkach: dwa rozne filtry admina),
* `bench_licznik_wywolan.py` -- licznik budow ChangeList i konsumpcji
generatora filtra (obalil moja hipoteze o
podwojnym ChangeList -- memoizacja dziala).
`src/django_bpp/settings/bench.py` + `bench_prod.py`: DWA warianty
konfiguracji, i to rozroznienie jest sedno rzetelnosci pomiaru. `local.py`
NIE definiuje `CACHEOPS` wcale, a produkcja cache'uje `bpp.uczelnia`,
`bpp.jednostka`, `bpp.tytul` i pozostale slowniki -- czyli dokladnie te
modele, w ktore uderzaja znalezione N+1. Pomiar tylko na `local.py`
ZAWYZA zysk: changelist autorow pokazywal 1102 -> 96 zapytan bez cacheops,
a z produkcyjnymi regulami 27 -> 27, czyli zysk ZEROWY. `bench_prod.py`
czyta reguly z `production.py` przez AST (bez wykonywania modulu), zeby
nie mierzyc wlasnej, rozjezdzajacej sie kopii.
`docs/deweloper/benchmark-orm.md` opisuje procedure, oba warianty, pulapki
(dump jest w formacie katalogowym `pg_dump -Fd`, wiec `run-site --from-dump`
go nie przyjmie; cacheops pikluje instancje, wiec przy zmianie wersji
Django trzeba FLUSHDB) i -- najwazniejsze -- jak NIE dac sie oszukac
czasom: scenariusz kontrolny `admin: zrodlo (changelist)` nie jest tkniety
zadna optymalizacja i ma stale 16 zapytan, wiec jego rozrzut miedzy
przebiegami to zmierzona podloga szumu hosta.
`HANDOFF-pomiar-wydajnosci-2026-08-07.md`: liczby zapytan sa zmierzone
i pewne (powtorzyly sie w pieciu przebiegach), ale CZASOW nie udalo sie
zmierzyc rzetelnie -- host jest stale obciazony rownoleglymi sesjami
i suitami testowymi. Kontroler pokazal skok 320 -> 420 ms na NIEZMIENIONEJ
sciezce kodu, co dowodzi, ze roznice czasowe pochodza z hosta, a nie
z kodu. Handoff opisuje, co zostalo do zrobienia, jakie jest kryterium
sukcesu i siedem pulapek, w ktore juz wpadlem.
Nic z tego nie wplywa na kod produkcyjny: `bench/` jest poza
`testpaths = src`, a moduly settings dziedzicza po `local.py`, ktory
w produkcji sie nie laduje.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XWWemKMkQMQmHZZPSmCdid
Zbiera to, czego migracja na 6.1 NIE objela, a co warto rozwazyc — z dowodami z kodu, szacunkiem kosztu i warunkami wyjscia. Nie jest to lista przepisana z release notes: kazda pozycja byla sprawdzona w tym repozytorium. Osobna sekcja notuje tematy SPRAWDZONE I WYKLUCZONE (podpisane ciasteczka, PBKDF2, deprecacja list_select_related=True, GeneratedField), zeby nie wracaly w dyskusji. Zawiera tez: dlug techniczny z warunkami zdjecia (pin DRF z gita, shim itercompat, usuniety django-prometheus), deprecacje 6.1 trafiajace w kod z terminem na Django 7.0 wraz z lista miejsc, oraz opis znanego flake'a w komparator_pbn_udzialy. Dokument nie jest w nav mkdocs — tak samo jak pozostale dokumenty planowe i audytowe w docs/deweloper/. `mkdocs build --strict` przechodzi. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F134BWb3YoYPQ6zjzqoqds
* fix(admin): brakujace JOIN-y na listach jednostek i wydawnictw zwartych Dwie deklaracje `list_select_related` byly martwe — kod je deklarowal, a Django ich do zapytania nie wstawialo. Objaw: N+1 na changelistach, maskowany od #733 przez FETCH_PEERS (2 zapytania zamiast 2N), ale nadal falszywa deklaracja. 1. JednostkaAdmin — pulapka warunkowego `apply_select_related`. `ChangeList.get_queryset` (django/contrib/admin/views/main.py) aplikuje deklaracje WARUNKOWO: if not qs.query.select_related: qs = self.apply_select_related(qs) czyli TYLKO gdy queryset bazowy admina nie ma jeszcze zadnego `select_related`. A `JednostkaManager.get_queryset()` dokłada `.select_related("wydzial")` (zdenormalizowany self-FK), wiec warunek jest falszywy i CALA deklaracja admina przepada — do zapytania szedl sam `wydzial`. Leniwie, per wiersz, leciały wiec: * `rodzaj` — kolumna `list_display`, * `uczelnia` — czytana przez `Jednostka.__str__` (sprawdza `uzywaj_wydzialow`), czyli w KAZDYM wierszu, * `parent` — kolumna `parent_nazwa` (`item.parent.nazwa`), ktorej w deklaracji w ogole nie bylo. Nie ma tu zadnego bledu ani ostrzezenia — z samego diffa/kodu admina tego nie widac, bo deklaracja wyglada poprawnie. Nadpisujemy wiec `get_queryset` i dokladamy `select_related` wprost (select_related sie scala, wiec `wydzial` z managera nie ginie) oraz dopisujemy `parent`. Przeglad calego `src/`: to JEDYNY manager w projekcie, ktory dokłada domyslny `select_related`, wiec pulapka wystepuje tylko tutaj. Pozostale `get_queryset` z `select_related` siedza na `ModelResource` (django-import-export) albo na adminach bez `list_select_related` — tam nie ma czego zgubic. 2. Wydawnictwo_ZwarteAdmin — deklaracja pod zla nazwa kolumny. BPP uzywa slownikowego dialektu `list_select_related` (django-dynamic-admin-columns): JOIN wchodzi tylko, gdy kolumna o danej nazwie jest widoczna. Wpis `"wydawca": ["wydawca"]` celowal w kolumne `wydawca`, ktorej NIE MA w `list_display_default`. Tymczasem po wydawce siega domyslnie widoczna kolumna `wydawnictwo` — property modelu `get_wydawnictwo()` sklada `self.wydawca.nazwa` z `wydawca_opis`. JOIN wchodzil wiec tylko przy recznie wlaczonej kolumnie `wydawca`, czyli praktycznie nigdy. Dokladamy `"wydawnictwo": ["wydawca"]`. Oba przypadki znalezione przez bramke FETCH_RAISE (kolejny commit). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F134BWb3YoYPQ6zjzqoqds * test(admin): bramka regresyjna na N+1 oparta o FETCH_RAISE (Django 6.1) Istniejacy `test_fetch_peers.py` sprawdza wylacznie, ze tryb FETCH_PEERS jest ustawiony i przezywa `_clone()`. To dowodzi, ze mechanizm jest podpiety — ale NIE dowodzi, ze N+1 faktycznie zniklo, i nie zatrzyma jego powrotu. Ktos dokłada kolumne do `list_display` albo relacje do `__str__`, liczba zapytan rosnie liniowo z liczba wierszy, a testy zostaja zielone. Nowy plik przybija KSZTALT ZAPYTAN czterech najgoretszych changelist (wydawnictwa ciagle i zwarte, autorzy, jednostki) trzema bramkami: 1. FETCH_RAISE. Renderujemy prawdziwa changeliste przez klienta HTTP, podmieniajac `bpp.admin.core.FETCH_PEERS` na FETCH_RAISE (monkeypatch na czas jednego testu — kod produkcyjny i DEFAULT_FETCH_MODE zostaja nietkniete). Wg kodu Django 6.1 tryb wedruje z `QuerySet._fetch_mode` przez `Model.from_db(fetch_mode=...)` do `instance._state.fetch_mode` (dziedzicza go tez obiekty z select_related i prefetch_related), a trzy deskryptory — DeferredAttribute, ForwardManyToOneDescriptor i ReverseOneToOneDescriptor — zamiast dociagac dane wołaja `fetch_mode.fetch(...)`. FetchRaise rzuca tam FieldFetchBlocked (podklasa FieldError) z komunikatem "Fetching of <Model>.<pole> blocked." Efekt: kazde leniwe dotkniecie relacji wywala test natychmiast, z nazwa pola — zamiast po cichu dolozyc N SELECT-ow. 2. Liczba zapytan niezalezna od liczby wierszy. Ta sama changelista z 2 i z 8 wierszami musi kosztowac DOKLADNIE tyle samo zapytan. To operacyjna definicja braku N+1 i — w odroznieniu od przybicia konkretnej liczby — nie wymaga aktualizacji przy niewinnych zmianach. Lapie tez to, czego FETCH_RAISE z zasady nie widzi: menedzery relacji odwrotnych (`obj.cos_set.all()`) i M2M tryb tylko DZIEDZICZA, nie sa przez niego blokowane. 3. Deklaracja `list_select_related` naprawde trafia do zapytania. Django aplikuje ja warunkowo (`if not qs.query.select_related`), wiec sama jej obecnosc w kodzie niczego nie gwarantuje — to wlasnie ta pulapka zjadla trzy JOIN-y na liscie jednostek (poprzedni commit). Test przechodzi po wszystkich adminach `bpp` i flaguje kazdego, ktorego queryset bazowy ma wlasny select_related, a zadeklarowanych sciezek w nim brakuje. Dane testowe maja WYPELNIONE FK widocznych kolumn — FETCH_RAISE odpala sie tylko dla relacji o niepustym kluczu (`has_value`), wiec rekordy z samymi NULL-ami przepuscilyby bramke na pusto. Bramka pilnuje tez samej siebie: `test_bramka_fetch_raise_faktycznie _gryzie` kasuje deklaracje `list_select_related` i wymaga, zeby FieldFetchBlocked FAKTYCZNIE poleciał. Bez tego caly plik moglby zrobic sie zielony na pusto, gdyby podmiana trybu przestala dzialac. Autouse fixture czysci `DynamicColumnsMixin._modeladmin_enabled` — to `cached_property` na singletonie admina, wiec przezywa rollback bazy i sprawia, ze kolejny test w module widzi UBOZSZY uklad kolumn, niz sadzi (czyli bramka cicho sie rozbraja). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01F134BWb3YoYPQ6zjzqoqds --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…o 6.1) (#734) Strona potwierdzenia kasowania w adminie enumeruje KAŻDY obiekt, który poleci kaskadą — z linkiem do zmiany, jeden `<li>` na sztukę. Dla `Autor`, `Zrodlo` czy masowego `delete_selected` na przefiltrowanej changeliście to dziesiątki tysięcy pozycji: kilkanaście MB HTML-a, którego przeglądarka nie renderuje sensownie, a człowiek i tak nie czyta. Django 6.1 dorzuciło na to `ModelAdmin.delete_confirmation_max_display`. Ustawiamy GLOBALNIE w `BaseBppAdminMixin` (100), a nie punktowo na paru „ciężkich" adminach: eksplozja listy nie bierze się z samego modelu, tylko z ogona jego kaskad, a ten potrafi urosnąć po dodaniu jednego FK w zupełnie innej aplikacji. Limit punktowy z definicji nie chroni tam, gdzie nikt problemu nie przewidział. 100 = wartość z przykładu w docstringu filtra Django i 2× `list_per_page`, więc cała strona changelisty zaznaczona do kasowania nadal wypisuje się co do sztuki. Sam atrybut NIE WYSTARCZA — i to jest istota tej zmiany. Django tylko wkłada tę wartość do kontekstu; o obcięciu decyduje szablon, wołając filtr `truncated_unordered_list`. Aktywną skórką admina jest grappelli, której `admin/delete_confirmation.html` i `admin/delete_selected_confirmation.html` renderują listę przez `|unordered_list` (bez obcinania) — a to grappelli, nie Django, wygrywa u nas rozwiązywanie nazwy szablonu. Dlatego oba nadpisania w `src/django_bpp/templates/admin/` przejmują blok `content` (grappelli trzyma tam całą treść i nie wystawia mniejszego bloku) i wołają filtr obcinający. Świadome odstępstwa od szablonów upstreamu: * `protected` i `perms_lacking` renderujemy BEZ obcinania. W BPP ta lista niesie ręcznie pisane komunikaty (`JednostkaAdmin.get_deleted_objects` dokłada je na KOŃCU), więc obcięcie ukryłoby dokładnie tę informację, po którą użytkownik na tę stronę przyszedł. * Sekcja „Podsumowanie" (`model_count`) zostaje nietknięta — pełne liczniki per model są nad listą, więc obcięcie zabiera tylko wyliczankę pojedynczych obiektów, a nie wiedzę o skali kasowania. Czego to NIE załatwia (żeby nie było złudzeń): opcja działa wyłącznie na etapie renderowania. `get_deleted_objects` nadal zbiera i formatuje komplet obiektów (`NestedObjects.collect` + `reverse()` per obiekt), więc zapytań do bazy ani szczytowego zużycia pamięci po stronie Pythona to nie zmniejsza. Zyskiem jest rozmiar odpowiedzi i to, że przeglądarka nie umiera. Istniejący `JednostkaAdmin.get_deleted_objects` nie koliduje: woła `super()` i dokłada wpisy do `protected`, a nowa opcja żyje wyłącznie po stronie szablonu. Testy idą przez klienta HTTP na realnym łańcuchu szablonów, bo asercja na samym atrybucie przechodziłaby również wtedy, gdyby ktoś skasował nadpisania szablonów, a strona dalej wypisywałaby wszystko. Claude-Session: https://claude.ai/code/session_01F134BWb3YoYPQ6zjzqoqds Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Pomiar wykonany na cichym hoscie, na kopii bazy produkcyjnej. Kryterium z handoffu spelnione: kontroler "admin: zrodlo (changelist)" ma 16 zapytan we wszystkich czterech przebiegach, rozrzut 3,2 ms = 1,1 % mediany, zero flag SZUM. Na mac-mini ten sam kontroler skakal 320 -> 420 ms, przez co tamten przebieg byl do wyrzucenia. Docstring BaseBppAdminMixin.get_queryset podawal 40 i 38 zapytan oraz czasy opisane jako rzad wielkosci -- podmienione na zmierzone (36 i 34). Dopisane dwie rzeczy, ktore pomiar ujawnil, a ktore zmieniaja uzasadnienie FETCH_PEERS: * changelist autorow ma 27 zapytan przed i po, a czas spada o 68 % -- bo dotykane relacje sa cache'owane przez CACHEOPS i koszt to round-tripy do Redisa, niewidoczne dla licznika SQL. Sam ten spadek pochodzi zreszta glownie z poprawek filtrow na dev (5ffc83f), nie z FETCH_PEERS. * PR #738 dokłada na dev jawne select_related dla tych samych relacji i schodzi do 16 i 35 zapytan -- o JEDNO mniej niz FETCH_PEERS, bo ten musi dorzucic zapytanie hurtowe na relacje, a JOIN nie. Po scaleniu te pozycje przestana byc argumentem za FETCH_PEERS; zostaje wlasciwy: siatka bezpieczenstwa na relacje, ktorych nikt nie zadeklarowal. Handoff przestawiony na ZAMKNIETY, z tabela czasow, rozbiciem 5.2 vs 6.1 i nowa pulapka: bench.py ustawia porty przez os.environ.setdefault(), co NIE nadpisuje zmiennej juz wyeksportowanej przez profil shella -- benchmark po cichu uderza wtedy w dev-owe kontenery zamiast w odtworzony dump. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PjoMtF6KAaJydhGEGibgV7
# Conflicts: # pyproject.toml # src/bpp/admin/jednostka.py
Django 6.1
Aktualizacja BPP z Django 5.2.16 LTS do Django 6.1 (wydane 2026-08-05).
Co się zmienia
requires-pythonpodniesione do >= 3.12 (wymóg 6.1; 3.11 wypadło razem z 6.0). Obraz Dockera i główne CI już stały na 3.12, więc nic tam nie trzeba ruszać.django-prometheususunięty. Miał twardy capDjango<6.1. Integracja była w całości opcjonalna (DJANGO_BPP_ENABLE_PROMETHEUS, domyślnieFalse) — poszła razem z flagą, middleware, wpisem wINSTALLED_APPSi routingiem/metrics.django-redis-iplwebna upstreamowydjango-redis. Fork był czystym republishem (zero kodu iplweb, ten sam modułdjango_redis) i utknął naDjango<6.1; upstream 7.0.0 deklarujeDjango<7.0,>=5.2. Nazwa backendu w konfiguracji się nie zmienia, więc wdrożenia nie wymagają żadnej akcji.Naprawione regresje Django 6.1
Każda potwierdzona kontrolą: test przechodzi na 5.2, oblewa na 6.1, przy niezmienionym otoczeniu.
1.
ForwardManyToOneDescriptor.get_queryset()wymagainstance(bpp/admin/core.py)W 6.1 metoda dokłada
.fetch_mode()ze stanu instancji, więc ma sygnaturę keyword-onlyinstance. BPP wołało ją w czasie definicji klasy formularza, gdzie instancji nie ma. Zastąpione sięgnięciem po_base_managerwprost — dokładnie to, co deskryptor robił pod spodem._base_manager, nie_default_manager: pole musi widzieć rekordy odfiltrowane przez managera domyślnego (soft-delete).2. Aliasy SELECT-a są konsekwentnie cytowane (
raport_slotow/core.py)ProgrammingError: kolumna "col2" nie istnieje.Autor_Dyscyplina.Meta.ordering = ('rok',), arokto druga kolumna wvalues(). Wymagało trzech pustych.order_by(): na obu operandachunion(), na jego wyniku i na wynikudifference()— ten ostatni dlatego, żeraport_slotow/models/uczelnia.pywoła na nimvalues_list(), co klonuje zapytanie razem z flagą domyślnego sortowania.3.
BaseModelForm.validate_constraints()— metoda nowa w 6.1 (bpp/admin/projekt.py)W 5.2 nie istniała;
Model.validate_constraintsjest w obu wersjach identyczne. Skutek: podmiana kierownika projektu w jednym zapisie przestawała przechodzić, bo stary wiersz oznaczonyDELETEwciąż jest w bazie. Django koordynuje walidację między formularzami tylko dla unikalności (BaseModelFormSet.validate_unique()odsiewadeleted_forms); dla constraintów odpowiednika nie ma. Walidacja per-wiersz wyłączona w dedykowanymProjekt_AutorForm; odsiew robi istniejącyProjekt_AutorFormSet.clean(), a constraint w bazie zostaje jako ostatnia linia obrony.4.
Script.__str__renderuje przez staticfiles storage (bpp/tests/test_zapytanie.py)Do 6.0
str(Script(...))nie zawierało content-hasha; teraz zawiera (highlight.js→highlight.f65f9cbe99ea.js). Asercja dopasowuje nazwę tolerując hash, zamiast porównywać literał.Dług techniczny wprowadzony świadomie
djangorestframeworkz gita (80d535dd)cc_delim_re, usunięte w 6.1 →ImportErrorna całymapi_v1. Fix jest namainupstreamu, ale 3.18.0 nie zostało wydane.pip-audit(URL requirements) i buduje się ze źródeł. Pin wskazuje na SHA namain, więc jest trwały (w odróżnieniu od gałęzi feature'owej). Wymiana na>=3.18to jedna linijka, gdy wydanie się pojawi.django-pbn-clientz gitadjango<5.3.ee5e81a35).django.utils.itercompatwdjango_bpp/compat.pydjango-admin-tools0.9.3 (ostatnie wydanie: sierpień 2023, klasyfikatory do Django 4.0) ma martwy import usuniętego modułu.django-admin-toolsalbo po forku z usuniętą tą linią.Dodatkowo podniesiony floor
h2>=4.4.1(CVE-2026-71554) wconstraint-dependencies. CVE dotyczyło tak samodev— tamh2również było 4.3.0; złapane przy okazji, bodevnie miał przebiegupip-auditod publikacji CVE.Przed produkcją warto obejrzeć panel admina — Django 6.1 przebudowało układ formularzy admina (usunięte
wide,object-toolspoza blokiemcontent), a w repo jest 64 nadpisanych szablonów. Testy sprawdzają zachowanie, nie layout. Szczegóły w komentarzu do PR-a.Czego świadomie NIE ma w tym PR
BLANK_CHOICE_DASH,transaction.savepoint(), gołeselect_related(),action_location,EMAIL_*→MAILERS). To warningi, nie błędy —filterwarnings = default, więc nic nie blokują. Osobny PR, żeby ten miał jeden czytelny sygnał.docs.yml(Python 3.11): ten workflow nie instaluje projektu, tylkodocs/requirements.txt+mkdocs build, więcrequires-pythongo nie dotyczy.Weryfikacja
manage.py check— czysto.makemigrations --check— dryf istnieje (favicon,flexible_reports,siteblog, wewnątrz.venv), ale identyczny na 5.2: stan zastany, nie skutek tego PR-a.uv.lock: z 365 pakietów zniknęło sześć — pięć backportów dla Pythona <3.12 (async-timeout,backports-tarfile,importlib-metadata,tomli,zipp) plus podmieniony fork Redisa. Reszta dużego diffu to metadane gałęzi rozwiązania dla 3.10/3.11.