Skip to content

refactor(ewaluacja): okres ewaluacyjny jako jedna stała, nie ~95 literałów#683

Open
mpasternak wants to merge 2 commits into
devfrom
refactor-okres-ewaluacji
Open

refactor(ewaluacja): okres ewaluacyjny jako jedna stała, nie ~95 literałów#683
mpasternak wants to merge 2 commits into
devfrom
refactor-okres-ewaluacji

Conversation

@mpasternak

@mpasternak mpasternak commented Jul 25, 2026

Copy link
Copy Markdown
Member

Po co

Wchodzimy w nowy okres ewaluacyjny (od 2026 r.), ale nie wiadomo jeszcze, czy będzie cztero- czy pięcioletni. Dziś okres 2022–2025 jest powielony w ~95 miejscach w pięciu aplikacjach ewaluacyjnych — jako domyślne argumenty funkcji, inline'owe filtry zapytań, stringi DjangoQL, listy lat, a nawet w nazwach funkcji.

Celem jest doprowadzenie do stanu, w którym przestawienie okresu to zmiana jednej linii, a stare raporty nadal dają się odtworzyć według starych zasad.

To jest refaktor pod przyszłą zmianę, nie sama zmiana. Okres pozostaje 2022–2025, wyniki liczbowe są identyczne, migracji nie ma.

Jak

# ewaluacja_common/const.py
OKRES_2022_2025 = (2022, 2025)   # zamknięty; zasady sprzed nowelizacji
OKRES_DOMYSLNY = OKRES_2022_2025 # <- TA JEDNA LINIA
ROK_MIN, ROK_MAX = OKRES_DOMYSLNY

Okresy mają nazwy, nie tylko wartości. Gdy MEiN ogłosi zasady, dopisujemy nową stałą, a OKRES_2022_2025 zostaje nietknięty — dane policzone pod stare zasady muszą dać się odtworzyć, co było wprost wymaganiem.

Dodatkowo lata_okresu() trzyma jedyne +1 wynikające z tego, że okres jest przedziałem domkniętym, a range() ma prawy koniec wyłączny. Ta jedynka była wcześniej powtarzana w kilku miejscach — czyli w kilku miejscach można się było w niej pomylić.

Dwie fazy

bf432cb6e — domyślne argumenty i nazwy funkcji

22 podstawienia + wartości pola formularza wyjęte z HTML-a do kontekstu widoku. Do tego lata wyjęte z nazw:

  • oblicz_liczby_n_dla_ewaluacji_2022_2025oblicz_liczby_n_dla_okresu
  • oblicz_liczbe_n_na_koniec_2025oblicz_liczbe_n_na_koniec_okresu

To groźniejsze niż zaszyte wartości: funkcja nazwana ..._2022_2025, która po zmianie okresu liczy 2026–2029, aktywnie wprowadza w błąd czytającego kod.

519162d14 — filtry zapytań

47 miejsc w 17 plikach: filtry ORM (rok__gte/rok__lte i warianty z prefiksami), stringi DjangoQL w liczbie N, listy i zakresy lat, domyślne argumenty argparse sterujące realnym UPDATE-em.

To była kategoria o największym ryzyku — po przestawieniu stałej te miejsca dalej liczyłyby stary okres po cichu, bez żadnego sygnału błędu.

Przy okazji: tasks/helpers.py miało rok__lt=2026 — jedyne miejsce zapisane „od drugiej strony". Wyprostowane na rok__lte=OKRES_DOMYSLNY[1]; dla lat całkowitych semantycznie identyczne.

Znalezione przy okazji: test, który nigdy się nie wykonał

test_get_lista_prac_zakres_lat miał w środku:

# The cache was not properly populated, skip this test for now
pytest.skip(...)

i wchodził w tę furtkę zawsze — bo budował Autor_Dyscyplina bez rodzaj_autora, przez co kalkulator slotów odrzucał autora i cache zostawał pusty. Asercja == 5 była martwa od momentu napisania.

Furtka usunięta, test ożywiony. Asercja wzmocniona, nie osłabiona: zamiast liczby prac sprawdza konkretne lata ([2022, 2023, 2024, 2025]), bo sama liczba nie wykryłaby zjechania zakresu o rok — 2023–2026 też daje cztery.

Ten test wykrył też realną niespójność: ROK_MAX wynosił 2026, choć cała reszta kodu ewaluacyjnego liczyła do 2025. Jedynym konsumentem tych stałych jest get_lista_prac(), niewołane produkcyjnie, więc naprawa nie zmienia dziś niczego — rozbraja pułapkę, zanim ktoś tę funkcję podepnie.

Czego świadomie NIE ruszam

Kategoria Ile Dlaczego
Etykiety UI, nazwy plików eksportu (liczba_n_ewaluacja_2022_2025.xlsx, "year_range": "2022-2025") ~32 Nie liczą źle. Ich nazwanie wymaga decyzji, jak ma się nazywać nowy okres — a to zależy od zasad, których jeszcze nie ma. Decyzja właściciela repo.
Literały 2022/2025 w testach Test ma przypinać jawną wartość. Podstawienie stałej sprawiłoby, że przestałby wykrywać przypadkową zmianę okresu.
verbose_name = "Dyscyplina nieraportowana 2022-2025" 1 Ten model faktycznie dotyczy tamtego okresu, więc nazwa jest prawdziwa. Zmiana wygenerowałaby zbędną migrację AlterModelOptions.
get_lista_prac() Nie ma produkcyjnych wywołań i jest kandydatem do usunięcia, ale kasowanie kodu to osobna decyzja.

Testy strażnicze

Dwa nowe testy podmieniają OKRES_DOMYSLNY przez mock.patch i sprawdzają, że zapytanie faktycznie filtruje po nowym okresie:

  • ewaluacja_liczba_n/tests/test_okres_w_zapytaniach.py — zakłada udziały za oba okresy, patchuje na (2026, 2029), asertuje wynik
  • ewaluacja_optymalizacja/tests/test_okres_w_zapytaniach.pylata_okresu() jako przedział domknięty (błąd o jeden) + filtr przeglądarki publikacji

Łapią też przypadek, w którym wartość zostałaby zapieczona w domyślnym argumencie — wtedy patch nie zadziała i test padnie. Zweryfikowane mutacyjnie: po cofnięciu zmian do literałów oba padają.

Weryfikacja

  • 223 testy przechodzą w pięciu aplikacjach ewaluacyjnych — to jest pełny zasięg zmiany, bo diff nie wychodzi poza src/ewaluacja_*
  • makemigrations --check --dry-run → brak nowych migracji
  • manage.py check czysty
  • ruff check i ruff format --check czyste na dotkniętych plikach

Pełna regresja repo: przerwana na 74%, świadomie. Docker na maszynie deweloperskiej zdegradował się w trakcie sesji (przestał montować pliki z dysku, a baza testowa zaczęła pełzać na TRUNCATE w oczekiwaniu na I/O — 65 minut na 74% przy 15 minutach dla identycznego przebiegu wcześniej tego samego dnia). To ograniczenie środowiska, nie kodu; pełną regresję zweryfikuje CI.

Do momentu przerwania padły 2 testy: api_v1/tests/test_n_plus_jeden.py::test_praca_doktorska_lista_bez_n_plus_jeden i ::test_patent_lista_bez_n_plus_jeden. Zdiagnozowane do końca — to artefakt środowiska deweloperskiego, nie błąd w kodzie:

  • na czystym origin/dev padają dokładnie te same dwa (2 failed, 10 passed na obu gałęziach), a ta gałąź nie dotyka api_v1;
  • CI na dev jest zielone, więc w poprawnym środowisku przechodzą;
  • przyczyna: oba modele mają charakter_formalny jako cached_property robiące Charakter_Formalny.objects.get(skrot="D"/"PAT"), czyli wymagają słownika z baseline'u. W zastępczej bazie testowej (postawionej ręcznie po awarii Dockera) baseline się nie załadował — sprawdzone: Charakter_Formalny.objects.count() == 1 zamiast 27.

Nie ma tu nic do naprawienia w repozytorium.

⚠ Konflikt przy scalaniu z PR #682

Ta gałąź i #682 (raport kompletności POL-on) obie dopisują stałe do src/ewaluacja_common/const.py. Konflikt jest czysto tekstowy, nie logiczny — rozwiązanie brzmi „zachowaj oba dopiski".

Stałe są celowo rozdzielne i opisują dwa różne zegary:

  • OKRES_DOMYSLNY — zakres liczenia punktacji (metryki, liczba N)
  • OKNO_EWALUACJI — zakres obowiązku sprawozdawczego z rozporządzenia POL-on

Sklejenie ich byłoby błędem: raport POL-on ma patrzeć od 2026 niezależnie od tego, za jakie lata liczone są metryki.

Refs FD#437

🤖 Generated with Claude Code

https://claude.ai/code/session_01McWgJYK4c4GSMdsm6P4oXc

mpasternak and others added 2 commits July 25, 2026 10:12
Okres 2022-2025 był powielony jako domyślne argumenty w czterech aplikacjach
ewaluacyjnych, w wartościach pola formularza i w NAZWACH funkcji. Zmiana
okresu wymagałaby polowania po repo, a funkcja nazwana
oblicz_liczby_n_dla_ewaluacji_2022_2025 licząca okres 2026-2029 aktywnie
wprowadzałaby w błąd.

OKRES_2022_2025 = (2022, 2025) jako okres nazwany i zamknięty (stare raporty
muszą dać się odtworzyć), OKRES_DOMYSLNY jako wskaźnik na okres bieżący.
ROK_MIN/ROK_MAX wyprowadzone z OKRES_DOMYSLNY, żeby nie mogły znów zdryfować
— ROK_MAX wynosił 2026, choć reszta kodu liczyła do 2025. Jedynym
konsumentem tych stałych jest get_lista_prac(), niewołane produkcyjnie, więc
poprawka nie zmienia niczego dziś, tylko rozbraja pułapkę.

Zero zmian wyników liczbowych — sygnatury pozostają dwuargumentowe, zmienia
się wyłącznie źródło wartości domyślnej. Bez migracji.

Przy okazji ożywiony test_get_lista_prac_zakres_lat: robił pytest.skip
("cache was not properly populated") ZAWSZE, bo tworzył Autor_Dyscyplina bez
rodzaj_autora, więc kalkulator slotów odrzucał autora i cache zostawał pusty.
Asercja nigdy się nie wykonała. Furtka usunięta, asercja wzmocniona z liczby
prac na konkretne lata — sama liczba nie wykryłaby zjechania zakresu o rok.

Literały 2022/2025 w testach zostawione celowo: test ma przypinać jawną
wartość, inaczej przestaje wykrywać przypadkową zmianę okresu.

Refs FD#437

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01McWgJYK4c4GSMdsm6P4oXc
Druga faza centralizacji. Pierwsza objęła domyślne argumenty funkcji i nazwy;
zostawały inline'owe literały w zapytaniach — kategoria groźniejsza, bo po
przestawieniu OKRES_DOMYSLNY liczyłyby po cichu stary okres, bez żadnego
sygnału błędu.

47 miejsc w 17 plikach: filtry ORM (rok__gte/lte i warianty z prefiksami),
stringi DjangoQL w liczbie N, listy i zakresy lat, domyślne argumenty
argparse sterujące realnym UPDATE-em.

lata_okresu() w ewaluacja_common trzyma jedyne "+1" wynikające z tego, że
okres jest przedziałem domkniętym, a range() ma prawy koniec wyłączny — żeby
ta jedynka nie była powtarzana i mylona w każdym miejscu budującym listę lat.
Czyta OKRES_DOMYSLNY przy wywołaniu, nie przy definicji, więc da się ją
podmienić w teście.

tasks/helpers.py miało rok__lt=2026 — jedyne miejsce zapisane "od drugiej
strony"; wyprostowane na rok__lte=OKRES_DOMYSLNY[1], semantycznie identyczne
dla lat całkowitych.

Etykiety UI i nazwy plików eksportu ("liczba_n_ewaluacja_2022_2025.xlsx",
"year_range": "2022-2025") celowo nietknięte — nie liczą źle, a nazwanie ich
wymaga decyzji, jak ma się nazywać nowy okres.

Dwa testy strażnicze podmieniają OKRES_DOMYSLNY przez mock.patch i sprawdzają,
że zapytanie faktycznie filtruje po nowym okresie. Łapią też przypadek, w
którym wartość zostałaby zapieczona w domyślnym argumencie — wtedy patch nie
zadziała. Zweryfikowane mutacyjnie.

223 testy w pięciu aplikacjach ewaluacyjnych. Bez migracji.

Refs FD#437

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