diff --git a/src/ewaluacja_common/const.py b/src/ewaluacja_common/const.py index dcfeaa70b..c49543d5a 100644 --- a/src/ewaluacja_common/const.py +++ b/src/ewaluacja_common/const.py @@ -1,3 +1,18 @@ +# Okres ewaluacyjny — JEDNO miejsce, z którego bierze się zakres lat. +# +# Okresy mają NAZWY, nie tylko wartości: gdy MEiN ogłosi zasady kolejnego +# okresu (2026+, cztero- albo pięcioletniego), dopisujemy tutaj NOWĄ stałą, +# a stara ZOSTAJE. Dane i raporty policzone pod poprzednie zasady muszą dać +# się odtworzyć, więc ``OKRES_2022_2025`` nigdy nie zmieni wartości — to +# okres zamknięty, liczony wg zasad sprzed nowelizacji. +OKRES_2022_2025 = (2022, 2025) + +# Okres, którego domyślnie używa kod produkcyjny. TO JEST TA JEDNA LINIA, +# którą przestawia się przy zmianie okresu ewaluacyjnego — wystarczy +# wskazać tu inną stałą ``OKRES_*``. Nie wpisuj tutaj krotki wprost; +# nazwany okres wyżej mówi, WEDŁUG JAKICH ZASAD liczymy. +OKRES_DOMYSLNY = OKRES_2022_2025 + # Zakres lat dla wyszukiwania prac do ewaluacji. # # Stałe te mieszkały historycznie w ``ewaluacja2021.const`` (apka raportów @@ -5,5 +20,27 @@ # ewaluacyjnych (liczba_n, optymalizacja, metryki) i to on faktycznie używa # zakresu lat w ``get_lista_prac``. Przeniesione tutaj, by żywy kod nie zależał # od uśpionej apki ``ewaluacja2021`` (patrz ``ewaluacja2021/README.md``). -ROK_MIN = 2022 -ROK_MAX = 2026 +# +# Wyprowadzone z ``OKRES_DOMYSLNY``, żeby nie mogły ponownie zdryfować +# względem reszty repo (przed refaktorem ``ROK_MAX`` wynosił tu 2026, choć +# cała reszta kodu ewaluacyjnego liczyła do 2025). +ROK_MIN, ROK_MAX = OKRES_DOMYSLNY + + +def lata_okresu(okres=None): + """Zwraca listę kolejnych lat okresu — dla filtrów ``rok__in`` i list w UI. + + Okres jest przedziałem DOMKNIĘTYM (``(2022, 2025)`` to cztery lata, + z rokiem 2025 włącznie), a ``range`` ma prawy koniec wyłączny — stąd + ``+ 1``. Ta jedynka mieszka wyłącznie tutaj, żeby nie trzeba jej było + powtarzać (i mylić się w niej) w każdym miejscu budującym listę lat. + + Args: + okres: krotka ``(rok_min, rok_max)``; domyślnie ``OKRES_DOMYSLNY`` + odczytywany w momencie WYWOŁANIA (a nie definicji funkcji), żeby + testy mogły podmienić okres przez ``mock.patch``. + """ + if okres is None: + okres = OKRES_DOMYSLNY + rok_min, rok_max = okres + return list(range(rok_min, rok_max + 1)) diff --git a/src/ewaluacja_dwudyscyplinowcy/core.py b/src/ewaluacja_dwudyscyplinowcy/core.py index c5821e841..bee0859bd 100644 --- a/src/ewaluacja_dwudyscyplinowcy/core.py +++ b/src/ewaluacja_dwudyscyplinowcy/core.py @@ -9,6 +9,7 @@ from bpp.models import Autor_Dyscyplina, Wydawnictwo_Ciagle_Autor from bpp.models.sloty.core import CannotAdapt, ISlot +from ewaluacja_common.const import lata_okresu logger = logging.getLogger(__name__) @@ -18,7 +19,8 @@ def pobierz_autorow_z_dwiema_dyscyplinami(lata=None): Pobiera autorów z dokładnie dwiema dyscyplinami dla podanych lat. Args: - lata: Lista lat do sprawdzenia. Domyślnie 2022-2025. + lata: Lista lat do sprawdzenia. Domyślnie lata bieżącego okresu + ewaluacji (``OKRES_DOMYSLNY``). Returns: Słownik {autor_id: { @@ -34,7 +36,7 @@ def pobierz_autorow_z_dwiema_dyscyplinami(lata=None): }} """ if lata is None: - lata = range(2022, 2026) # 2022-2025 + lata = lata_okresu() autorzy_dict = defaultdict(lambda: {"autor": None, "lata": {}}) diff --git a/src/ewaluacja_liczba_n/excel_export.py b/src/ewaluacja_liczba_n/excel_export.py index 529d6e0f0..64711200f 100644 --- a/src/ewaluacja_liczba_n/excel_export.py +++ b/src/ewaluacja_liczba_n/excel_export.py @@ -10,7 +10,7 @@ from bpp.models import Uczelnia from .models import LiczbaNDlaUczelni -from .utils import oblicz_liczbe_n_na_koniec_2025 +from .utils import oblicz_liczbe_n_na_koniec_okresu class LiczbaNExcelExporter: @@ -84,7 +84,7 @@ def _create_nieraportowane_worksheet( .select_related("dyscyplina_naukowa") .order_by("dyscyplina_naukowa__nazwa") ) - liczby_n_2025 = oblicz_liczbe_n_na_koniec_2025(uczelnia) + liczby_n_2025 = oblicz_liczbe_n_na_koniec_okresu(uczelnia) # Filter only non-reported (N < 12 at end of 2025) row_num = 2 diff --git a/src/ewaluacja_liczba_n/management/commands/przelicz_n.py b/src/ewaluacja_liczba_n/management/commands/przelicz_n.py index d788ec6e1..b7c57eefd 100644 --- a/src/ewaluacja_liczba_n/management/commands/przelicz_n.py +++ b/src/ewaluacja_liczba_n/management/commands/przelicz_n.py @@ -1,7 +1,7 @@ from django.core.management import BaseCommand from bpp.models import Uczelnia -from ewaluacja_liczba_n.utils import oblicz_liczby_n_dla_ewaluacji_2022_2025 +from ewaluacja_liczba_n.utils import oblicz_liczby_n_dla_okresu class Command(BaseCommand): @@ -24,5 +24,5 @@ def handle(self, *args, **options): uczelnia = Uczelnia.objects.get() self.stdout.write("Przeliczam liczby N dla uczelni...") - oblicz_liczby_n_dla_ewaluacji_2022_2025(uczelnia=uczelnia) + oblicz_liczby_n_dla_okresu(uczelnia=uczelnia) self.stdout.write(self.style.SUCCESS("Przeliczono liczby N pomyślnie!")) diff --git a/src/ewaluacja_liczba_n/tests/test_core_util.py b/src/ewaluacja_liczba_n/tests/test_core_util.py index a52f3a157..2e0a5e6f0 100644 --- a/src/ewaluacja_liczba_n/tests/test_core_util.py +++ b/src/ewaluacja_liczba_n/tests/test_core_util.py @@ -13,11 +13,14 @@ def test_get_lista_prac_zakres_lat( denorms, typy_odpowiedzialnosci, charaktery_formalne, + rodzaj_autora_n, ): - """Sprawdza, czy lista prac odrzuca prace spoza zakresu 2022-2026""" + """Sprawdza, czy lista prac odrzuca prace spoza okresu ewaluacji 2022-2025""" from bpp.models import Charakter_Formalny - # Zrob dane testowe od 2015 do 2026 + # Dane testowe od 2015 do 2026 — z zapasem po OBU stronach okresu + # ewaluacji, żeby test wyłapał zjechanie zarówno dolnej, jak i górnej + # granicy zakresu. LiczbaNDlaUczelni.objects.create( dyscyplina_naukowa=dyscyplina1, uczelnia=uczelnia, liczba_n=100 @@ -29,8 +32,14 @@ def test_get_lista_prac_zakres_lat( ).first() for ROK in range(2015, 2027): + # rodzaj_autora jest OBOWIĄZKOWY: bez rodzaju z ``licz_sloty=True`` + # ``SlotMixin.autorzy_z_dyscypliny`` pomija autora, przez co rekord + # nie trafia do ``Cache_Punktacja_Autora`` i test nie ma czego badać. Autor_Dyscyplina.objects.create( - autor=autor_jan_nowak, rok=ROK, dyscyplina_naukowa=dyscyplina1 + autor=autor_jan_nowak, + rok=ROK, + dyscyplina_naukowa=dyscyplina1, + rodzaj_autora=rodzaj_autora_n, ) # Create IloscUdzialowDlaAutoraZaRok only for years 2022-2026 @@ -46,6 +55,10 @@ def test_get_lista_prac_zakres_lat( wc: Wydawnictwo_Ciagle = baker.make( Wydawnictwo_Ciagle, rok=ROK, + # 5 pkt wpada w próg 3 kalkulatora slotów, ale DOPIERO od 2017 r. + # (patrz ``bpp.models.sloty.core._dopasuj_kalkulator``); lata + # 2015-2016 nie dają się przeliczyć i cache ich nie zawiera — + # to nie szkodzi, bo i tak są poza okresem ewaluacji. punkty_kbn=5, tytul_oryginalny=f"Test 123 - praca za rok {ROK}", charakter_formalny=charakter_formalny, @@ -54,19 +67,17 @@ def test_get_lista_prac_zakres_lat( denorms.flush() - # Debug: Check if cache entries were created + # Bez wpisów w cache test nie badałby niczego — sprawdzamy to JAWNIE, + # zamiast (jak dawniej) po cichu robić ``pytest.skip``, przez co asercja + # niżej nigdy się nie wykonywała. from bpp.models import Cache_Punktacja_Autora_Query - cache_entries = Cache_Punktacja_Autora_Query.objects.filter( + assert Cache_Punktacja_Autora_Query.objects.filter( dyscyplina__nazwa=dyscyplina1.nazwa, autor=autor_jan_nowak - ) - # If no cache entries, the test cannot pass - if cache_entries.count() == 0: - # The cache was not properly populated, skip this test for now - import pytest - - pytest.skip( - "Cache_Punktacja_Autora_Query not populated - this appears to be a test infrastructure issue" - ) + ).exists(), "Cache_Punktacja_Autora_Query pusty — test nie ma czego sprawdzać" - assert (len(list(get_lista_prac(dyscyplina1.nazwa)))) == 5 + # Asercja na KONKRETNYCH latach, nie na samej ich liczbie: gdyby zakres + # zjechał o rok w którąkolwiek stronę (np. 2023-2026), sama liczba prac + # nadal wynosiłaby 4 i test by tego nie zauważył. + lata = sorted(praca.rok for praca in get_lista_prac(dyscyplina1.nazwa)) + assert lata == [2022, 2023, 2024, 2025] diff --git a/src/ewaluacja_liczba_n/tests/test_okres_w_zapytaniach.py b/src/ewaluacja_liczba_n/tests/test_okres_w_zapytaniach.py new file mode 100644 index 000000000..54c48b4de --- /dev/null +++ b/src/ewaluacja_liczba_n/tests/test_okres_w_zapytaniach.py @@ -0,0 +1,79 @@ +"""Strażnik: zapytania LICZĄCE muszą brać zakres lat z ``OKRES_DOMYSLNY``. + +Okres 2022-2025 był wpisany na sztywno w kilkudziesięciu filtrach ORM. +To groźniejsza kategoria niż literał w etykiecie: po przestawieniu +``OKRES_DOMYSLNY`` na kolejny okres takie miejsce dalej liczyłoby po cichu +2022-2025 — bez wyjątku, bez ostrzeżenia, tylko ze złym wynikiem. + +Test celuje w najcięższe miejsce w apce ``ewaluacja_liczba_n`` — queryset +listy udziałów, z którego wyrasta cała liczba N — i sprawdza, że podmiana +stałej faktycznie zmienia to, co widok pobiera z bazy. Sam fakt, że w kodzie +stoi ``OKRES_DOMYSLNY[0]``, jest tu niewystarczający: liczy się, czy wartość +jest czytana w momencie WYWOŁANIA (a nie zapieczona np. w domyślnym +argumencie funkcji, gdzie ``mock.patch`` już by nie pomógł). +""" + +from decimal import Decimal +from unittest import mock + +import pytest +from django.test import RequestFactory +from model_bakery import baker + +from bpp.models import Autor, Dyscyplina_Naukowa, Jednostka, Uczelnia +from ewaluacja_liczba_n.models import IloscUdzialowDlaAutoraZaRok +from ewaluacja_liczba_n.views import list as widok_listy + +# Okres celowo rozłączny z 2022-2025 — gdyby widok dalej filtrował po starych +# latach, przecięcie zbiorów byłoby puste i test padnie jednoznacznie. +OKRES_TESTOWY = (2026, 2029) + +LATA_STAREGO_OKRESU = [2022, 2023, 2024, 2025] +LATA_TESTOWEGO_OKRESU = [2026, 2027, 2028, 2029] + + +@pytest.fixture +def uczelnia_z_udzialami_z_dwoch_okresow(db): + """Udziały za oba okresy naraz — po jednym rekordzie na każdy rok.""" + uczelnia = baker.make(Uczelnia) + jednostka = baker.make(Jednostka, uczelnia=uczelnia, skupia_pracownikow=True) + autor = baker.make(Autor, aktualna_jednostka=jednostka) + dyscyplina = baker.make(Dyscyplina_Naukowa) + + for rok in LATA_STAREGO_OKRESU + LATA_TESTOWEGO_OKRESU: + IloscUdzialowDlaAutoraZaRok.objects.create( + autor=autor, + dyscyplina_naukowa=dyscyplina, + uczelnia=uczelnia, + rok=rok, + ilosc_udzialow=Decimal("1.0"), + ilosc_udzialow_monografie=Decimal("0.5"), + ) + + return uczelnia + + +def _lata_widziane_przez_widok(uczelnia): + """Lata, które widok listy faktycznie wyciąga z bazy.""" + request = RequestFactory().get("/") + # Podstawiamy uczelnię wprost — ``get_for_request`` czyta ten cache jako + # pierwszy, więc omijamy rozwiązywanie domena → Site → Uczelnia. + request._uczelnia = uczelnia + + widok = widok_listy.AutorzyLiczbaNListView() + widok.request = request + return sorted(widok.get_queryset().values_list("rok", flat=True)) + + +@pytest.mark.django_db +def test_lista_udzialow_filtruje_po_okresie_domyslnym( + uczelnia_z_udzialami_z_dwoch_okresow, +): + uczelnia = uczelnia_z_udzialami_z_dwoch_okresow + + # Stan bazowy: bieżący OKRES_DOMYSLNY to 2022-2025. + assert _lata_widziane_przez_widok(uczelnia) == LATA_STAREGO_OKRESU + + # A teraz sedno: po podmianie okresu widok MUSI pokazać nowe lata. + with mock.patch.object(widok_listy, "OKRES_DOMYSLNY", OKRES_TESTOWY): + assert _lata_widziane_przez_widok(uczelnia) == LATA_TESTOWEGO_OKRESU diff --git a/src/ewaluacja_liczba_n/tests/test_per_uczelnia.py b/src/ewaluacja_liczba_n/tests/test_per_uczelnia.py index 006ca8899..9fbac3e27 100644 --- a/src/ewaluacja_liczba_n/tests/test_per_uczelnia.py +++ b/src/ewaluacja_liczba_n/tests/test_per_uczelnia.py @@ -75,7 +75,7 @@ def test_pipeline_izolacja_dwie_uczelnie(db): IloscUdzialowDlaAutoraZaRok, LiczbaNDlaUczelni, ) - from ewaluacja_liczba_n.utils import oblicz_liczby_n_dla_ewaluacji_2022_2025 + from ewaluacja_liczba_n.utils import oblicz_liczby_n_dla_okresu u1 = baker.make(Uczelnia, skrot="U1", nazwa="U1") u2 = baker.make(Uczelnia, skrot="U2", nazwa="U2") @@ -88,8 +88,8 @@ def test_pipeline_izolacja_dwie_uczelnie(db): for rok in (2022, 2023, 2024, 2025): _make_autor_dyscyplina(autor, rok, dyscyplina) - oblicz_liczby_n_dla_ewaluacji_2022_2025(u1) - oblicz_liczby_n_dla_ewaluacji_2022_2025(u2) # second run must NOT wipe u1 + oblicz_liczby_n_dla_okresu(u1) + oblicz_liczby_n_dla_okresu(u2) # second run must NOT wipe u1 assert IloscUdzialowDlaAutoraZaRok.objects.filter(uczelnia=u1, autor=a1).exists() assert IloscUdzialowDlaAutoraZaRok.objects.filter(uczelnia=u2, autor=a2).exists() @@ -108,7 +108,7 @@ def test_pipeline_pomija_nieprzypisanych(db): from bpp.models import Autor, Jednostka, Uczelnia from bpp.models.dyscyplina_naukowa import Dyscyplina_Naukowa from ewaluacja_liczba_n.models import IloscUdzialowDlaAutoraZaRok - from ewaluacja_liczba_n.utils import oblicz_liczby_n_dla_ewaluacji_2022_2025 + from ewaluacja_liczba_n.utils import oblicz_liczby_n_dla_okresu u1 = baker.make(Uczelnia, skrot="U1", nazwa="U1") obca = baker.make(Jednostka, uczelnia=u1, skupia_pracownikow=False) @@ -118,7 +118,7 @@ def test_pipeline_pomija_nieprzypisanych(db): for autor in (a_null, a_obca): _make_autor_dyscyplina(autor, 2022, dyscyplina) - oblicz_liczby_n_dla_ewaluacji_2022_2025(u1) + oblicz_liczby_n_dla_okresu(u1) assert not IloscUdzialowDlaAutoraZaRok.objects.filter(autor=a_null).exists() assert not IloscUdzialowDlaAutoraZaRok.objects.filter(autor=a_obca).exists() diff --git a/src/ewaluacja_liczba_n/tests/test_utils.py b/src/ewaluacja_liczba_n/tests/test_utils.py index c4cb3094e..0ba1b3b11 100644 --- a/src/ewaluacja_liczba_n/tests/test_utils.py +++ b/src/ewaluacja_liczba_n/tests/test_utils.py @@ -9,13 +9,13 @@ LiczbaNDlaUczelni, ) from ewaluacja_liczba_n.utils import ( - oblicz_liczby_n_dla_ewaluacji_2022_2025, + oblicz_liczby_n_dla_okresu, oblicz_srednia_liczbe_n_dla_dyscyplin, ) @pytest.mark.parametrize("zaokraglaj", [True, False]) -def test_oblicz_liczby_n_dla_ewaluacji_2022_2025_prosty( +def test_oblicz_liczby_n_dla_okresu_prosty( uczelnia, jednostka, autor_jan_nowak, @@ -57,7 +57,7 @@ def test_oblicz_liczby_n_dla_ewaluacji_2022_2025_prosty( uczelnia.przydzielaj_1_slot_gdy_udzial_mniejszy = zaokraglaj uczelnia.save() - oblicz_liczby_n_dla_ewaluacji_2022_2025(uczelnia) + oblicz_liczby_n_dla_okresu(uczelnia) assert ( IloscUdzialowDlaAutoraZaRok.objects.get(autor=autor_jan_nowak).ilosc_udzialow @@ -351,7 +351,7 @@ def test_autor_typu_z_ma_udzialy_zero( ) # Uruchom obliczenia - oblicz_liczby_n_dla_ewaluacji_2022_2025(uczelnia) + oblicz_liczby_n_dla_okresu(uczelnia) # Sprawdź że autor ma wpis w IloscUdzialowDlaAutoraZaRok assert IloscUdzialowDlaAutoraZaRok.objects.filter(autor=autor, rok=2025).exists() @@ -396,15 +396,15 @@ def test_autor_typu_z_nie_wliczany_do_liczby_n( ) # Uruchom obliczenia - oblicz_liczby_n_dla_ewaluacji_2022_2025(uczelnia) + oblicz_liczby_n_dla_okresu(uczelnia) # Sprawdź że wszystkie 25 autorów ma wpisy assert IloscUdzialowDlaAutoraZaRok.objects.filter(rok=2025).count() == 25 # Sprawdź że liczba N = 15 (tylko autorzy typu N) - from ewaluacja_liczba_n.utils import oblicz_liczbe_n_na_koniec_2025 + from ewaluacja_liczba_n.utils import oblicz_liczbe_n_na_koniec_okresu - liczby_n_2025 = oblicz_liczbe_n_na_koniec_2025(uczelnia) + liczby_n_2025 = oblicz_liczbe_n_na_koniec_okresu(uczelnia) assert liczby_n_2025.get(dyscyplina1.id, 0) == Decimal("15.0") @@ -440,7 +440,7 @@ def test_autor_zmienia_typ_z_n_na_z( ) # Uruchom obliczenia - oblicz_liczby_n_dla_ewaluacji_2022_2025(uczelnia) + oblicz_liczby_n_dla_okresu(uczelnia) # Sprawdź że w 2024 ma udziały > 0 udzial_2024 = IloscUdzialowDlaAutoraZaRok.objects.get(autor=autor, rok=2024) diff --git a/src/ewaluacja_liczba_n/utils.py b/src/ewaluacja_liczba_n/utils.py index 8481437a2..abbfd2b29 100644 --- a/src/ewaluacja_liczba_n/utils.py +++ b/src/ewaluacja_liczba_n/utils.py @@ -2,6 +2,8 @@ from django.db import transaction +from ewaluacja_common.const import OKRES_DOMYSLNY + from .models import ( IloscUdzialowDlaAutoraZaCalosc, IloscUdzialowDlaAutoraZaRok, @@ -10,7 +12,9 @@ @transaction.atomic -def oblicz_srednia_liczbe_n_dla_dyscyplin(uczelnia, rok_min=2022, rok_max=2025): +def oblicz_srednia_liczbe_n_dla_dyscyplin( + uczelnia, rok_min=OKRES_DOMYSLNY[0], rok_max=OKRES_DOMYSLNY[1] +): """ Oblicza średnią liczbę N dla każdej dyscypliny w przeliczeniu na pełny wymiar czasu pracy. @@ -88,14 +92,14 @@ def oblicz_srednia_liczbe_n_dla_dyscyplin(uczelnia, rok_min=2022, rok_max=2025): ) -def oblicz_dyscypliny_nieraportowane(uczelnia, rok=2025): +def oblicz_dyscypliny_nieraportowane(uczelnia, rok=OKRES_DOMYSLNY[1]): """ Oblicza zbiór ID dyscyplin nieraportowanych na podstawie sumy udziałów. Dyscyplina jest nieraportowana gdy suma udziałów < 12. Args: uczelnia: Uczelnia dla której wykonujemy obliczenia - rok: Rok dla którego sprawdzamy (domyślnie 2025) + rok: Rok dla którego sprawdzamy (domyślnie ostatni rok okresu) Returns: set: Zbiór ID dyscyplin nieraportowanych @@ -116,7 +120,10 @@ def oblicz_dyscypliny_nieraportowane(uczelnia, rok=2025): def dolicz_bonus_za_nieraportowana( - uczelnia, nieraportowane_ids, rok_min=2022, rok_max=2025 + uczelnia, + nieraportowane_ids, + rok_min=OKRES_DOMYSLNY[0], + rok_max=OKRES_DOMYSLNY[1], ): """ Dolicza +1 slot dla autorów z dwoma dyscyplinami, gdzie jedna jest nieraportowana. @@ -200,7 +207,9 @@ def dolicz_bonus_za_nieraportowana( @transaction.atomic -def oblicz_sumy_udzialow_za_calosc(uczelnia, rok_min=2022, rok_max=2025): +def oblicz_sumy_udzialow_za_calosc( + uczelnia, rok_min=OKRES_DOMYSLNY[0], rok_max=OKRES_DOMYSLNY[1] +): """ Oblicza sumę udziałów dla każdego autora, dyscypliny i rodzaju autora za cały okres ewaluacji, tylko dla danej uczelni. @@ -309,12 +318,16 @@ def oblicz_sumy_udzialow_za_calosc(uczelnia, rok_min=2022, rok_max=2025): ) -def oblicz_liczbe_n_na_koniec_2025(uczelnia): +def oblicz_liczbe_n_na_koniec_okresu(uczelnia, rok=OKRES_DOMYSLNY[1]): """ - Oblicza liczbę N dla każdej dyscypliny NA KONIEC 2025 ROKU (bez zapisywania do bazy). + Oblicza liczbę N dla każdej dyscypliny NA KONIEC OKRESU (bez zapisywania do bazy). - Funkcja pomocnicza używana do wyświetlania liczby N na koniec 2025 w interfejsie. - Zwraca słownik {dyscyplina_id: liczba_n_2025}. + Funkcja pomocnicza używana do wyświetlania liczby N na koniec okresu + ewaluacji w interfejsie. Zwraca słownik {dyscyplina_id: liczba_n}. + + Args: + uczelnia: Uczelnia dla której wykonujemy obliczenia + rok: Ostatni rok okresu ewaluacji (domyślnie z ``OKRES_DOMYSLNY``) UWAGA: Liczy NIEWAŻONĄ sumę udziałów (bez wymiar_etatu × procent_dyscypliny), tylko prosta suma ilosc_udzialow z tabeli IloscUdzialowDlaAutoraZaRok. @@ -323,21 +336,19 @@ def oblicz_liczbe_n_na_koniec_2025(uczelnia): from bpp.models.dyscyplina_naukowa import Autor_Dyscyplina - # Słownik do przechowywania sum udziałów dla każdej dyscypliny w roku 2025 - dyscyplina_stats_2025 = defaultdict(lambda: Decimal("0")) + # Słownik do przechowywania sum udziałów dla każdej dyscypliny w ostatnim roku + dyscyplina_stats = defaultdict(lambda: Decimal("0")) - # Pobierz wszystkie udziały dla autorów w roku 2025 dla tej uczelni - udzialy_2025 = IloscUdzialowDlaAutoraZaRok.objects.filter( - uczelnia=uczelnia, rok=2025 + # Pobierz wszystkie udziały dla autorów w ostatnim roku okresu dla tej uczelni + udzialy = IloscUdzialowDlaAutoraZaRok.objects.filter( + uczelnia=uczelnia, rok=rok ).select_related("autor", "dyscyplina_naukowa") # Dla każdego udziału sumuj nieważone udziały - for udzial in udzialy_2025: + for udzial in udzialy: try: - # Pobierz rodzaj autora dla autora w roku 2025 - autor_dyscyplina = Autor_Dyscyplina.objects.get( - autor=udzial.autor, rok=2025 - ) + # Pobierz rodzaj autora dla autora w ostatnim roku okresu + autor_dyscyplina = Autor_Dyscyplina.objects.get(autor=udzial.autor, rok=rok) # Tylko dla pracowników zaliczanych do liczby N if ( @@ -345,19 +356,19 @@ def oblicz_liczbe_n_na_koniec_2025(uczelnia): and autor_dyscyplina.rodzaj_autora.jest_w_n ): # Sumuj tylko ilosc_udzialow bez ważenia - dyscyplina_stats_2025[udzial.dyscyplina_naukowa_id] += ( - udzial.ilosc_udzialow - ) + dyscyplina_stats[udzial.dyscyplina_naukowa_id] += udzial.ilosc_udzialow except Autor_Dyscyplina.DoesNotExist: - # Jeśli nie ma przypisania dla autora w 2025, pomijamy + # Jeśli nie ma przypisania dla autora w tym roku, pomijamy continue - return dict(dyscyplina_stats_2025) + return dict(dyscyplina_stats) @transaction.atomic -def oblicz_liczby_n_dla_ewaluacji_2022_2025(uczelnia, rok_min=2022, rok_max=2025): +def oblicz_liczby_n_dla_okresu( + uczelnia, rok_min=OKRES_DOMYSLNY[0], rok_max=OKRES_DOMYSLNY[1] +): from bpp.models.dyscyplina_naukowa import Autor_Dyscyplina warunek_lat = dict(rok__gte=rok_min, rok__lte=rok_max) @@ -415,7 +426,7 @@ def oblicz_liczby_n_dla_ewaluacji_2022_2025(uczelnia, rok_min=2022, rok_max=2025 # Krok 2: Policz średnią dla dyscyplin (BEZ bonusu!) oblicz_srednia_liczbe_n_dla_dyscyplin(uczelnia, rok_min, rok_max) - # Krok 3: Określ dyscypliny nieraportowane (suma 2025 < 12) + # Krok 3: Określ dyscypliny nieraportowane (suma w ostatnim roku < 12) nieraportowane_ids = oblicz_dyscypliny_nieraportowane(uczelnia, rok_max) # Krok 4: Doliczyć +1 slot gdzie potrzeba (NA KOŃCU - nie wpływa na średnią!) diff --git a/src/ewaluacja_liczba_n/views/export.py b/src/ewaluacja_liczba_n/views/export.py index 41c9665f3..d763a9ae4 100644 --- a/src/ewaluacja_liczba_n/views/export.py +++ b/src/ewaluacja_liczba_n/views/export.py @@ -8,6 +8,7 @@ from bpp.const import GR_WPROWADZANIE_DANYCH from bpp.models import Uczelnia +from ewaluacja_common.const import OKRES_DOMYSLNY from ..excel_export import LiczbaNExcelExporter from ..models import IloscUdzialowDlaAutoraZaCalosc, IloscUdzialowDlaAutoraZaRok @@ -23,7 +24,7 @@ def _get_filtered_udzialy_queryset(self, request): """Get filtered queryset based on request parameters.""" uczelnia = Uczelnia.objects.get_for_request(request) udzialy = IloscUdzialowDlaAutoraZaRok.objects.filter( - uczelnia=uczelnia, rok__gte=2022, rok__lte=2025 + uczelnia=uczelnia, rok__gte=OKRES_DOMYSLNY[0], rok__lte=OKRES_DOMYSLNY[1] ) # Apply filters from URL diff --git a/src/ewaluacja_liczba_n/views/index.py b/src/ewaluacja_liczba_n/views/index.py index f0b019e09..4f3e1a164 100644 --- a/src/ewaluacja_liczba_n/views/index.py +++ b/src/ewaluacja_liczba_n/views/index.py @@ -15,8 +15,8 @@ from ..forms import SankcjeFormSet from ..models import LiczbaNDlaUczelni from ..utils import ( - oblicz_liczbe_n_na_koniec_2025, - oblicz_liczby_n_dla_ewaluacji_2022_2025, + oblicz_liczbe_n_na_koniec_okresu, + oblicz_liczby_n_dla_okresu, ) @@ -38,7 +38,7 @@ def get_context_data(self, **kwargs): ) # Oblicz liczby N na koniec 2025 dla każdej dyscypliny - liczby_n_2025 = oblicz_liczbe_n_na_koniec_2025(uczelnia) + liczby_n_2025 = oblicz_liczbe_n_na_koniec_okresu(uczelnia) # Dodaj liczby N na koniec 2025 do każdego obiektu i podziel na raportowane/nieraportowane liczby_n_raportowane = [] @@ -99,7 +99,7 @@ def post(self, request, *args, **kwargs): uczelnia = Uczelnia.objects.get_for_request(request) try: - oblicz_liczby_n_dla_ewaluacji_2022_2025(uczelnia) + oblicz_liczby_n_dla_okresu(uczelnia) messages.success( request, "Pomyślnie obliczono liczbę N dla ewaluacji 2022-2025" ) diff --git a/src/ewaluacja_liczba_n/views/list.py b/src/ewaluacja_liczba_n/views/list.py index f0dd650ba..9ee8b3e5d 100644 --- a/src/ewaluacja_liczba_n/views/list.py +++ b/src/ewaluacja_liczba_n/views/list.py @@ -6,6 +6,7 @@ from bpp.const import GR_WPROWADZANIE_DANYCH from bpp.models import Autor_Dyscyplina, Dyscyplina_Naukowa, Uczelnia +from ewaluacja_common.const import OKRES_DOMYSLNY from ewaluacja_common.models import Rodzaj_Autora from ..models import IloscUdzialowDlaAutoraZaCalosc, IloscUdzialowDlaAutoraZaRok @@ -30,8 +31,8 @@ def _filter_by_rodzaj_autora(self, queryset, rodzaj_autora_id, rok): if rok: ad_filter["rok"] = rok else: - ad_filter["rok__gte"] = 2022 - ad_filter["rok__lte"] = 2025 + ad_filter["rok__gte"] = OKRES_DOMYSLNY[0] + ad_filter["rok__lte"] = OKRES_DOMYSLNY[1] # Pobierz pary (autor_id, rok) z danym rodzajem autora autorzy_z_rodzajem = ( @@ -184,7 +185,7 @@ def get_queryset(self): uczelnia = Uczelnia.objects.get_for_request(self.request) # Pobierz wszystkie udziały dla autorów tej uczelni queryset = IloscUdzialowDlaAutoraZaRok.objects.filter( - uczelnia=uczelnia, rok__gte=2022, rok__lte=2025 + uczelnia=uczelnia, rok__gte=OKRES_DOMYSLNY[0], rok__lte=OKRES_DOMYSLNY[1] ).select_related( "autor", "dyscyplina_naukowa", @@ -212,7 +213,9 @@ def get_context_data(self, **kwargs): # Pobierz ID dyscyplin które mają faktyczne dane dla tej uczelni dyscypliny_z_danymi = ( IloscUdzialowDlaAutoraZaRok.objects.filter( - uczelnia=uczelnia, rok__gte=2022, rok__lte=2025 + uczelnia=uczelnia, + rok__gte=OKRES_DOMYSLNY[0], + rok__lte=OKRES_DOMYSLNY[1], ) .values_list("dyscyplina_naukowa_id", flat=True) .distinct() @@ -226,7 +229,9 @@ def get_context_data(self, **kwargs): # Pobierz tylko lata które faktycznie są w bazie dla tej uczelni lata_z_danymi = ( IloscUdzialowDlaAutoraZaRok.objects.filter( - uczelnia=uczelnia, rok__gte=2022, rok__lte=2025 + uczelnia=uczelnia, + rok__gte=OKRES_DOMYSLNY[0], + rok__lte=OKRES_DOMYSLNY[1], ) .values_list("rok", flat=True) .distinct() diff --git a/src/ewaluacja_liczba_n/views/verify.py b/src/ewaluacja_liczba_n/views/verify.py index 578af341f..c0953d20f 100644 --- a/src/ewaluacja_liczba_n/views/verify.py +++ b/src/ewaluacja_liczba_n/views/verify.py @@ -10,6 +10,7 @@ from bpp.const import GR_WPROWADZANIE_DANYCH from bpp.models import Autor_Dyscyplina, Uczelnia +from ewaluacja_common.const import OKRES_DOMYSLNY from ewaluacja_common.models import Rodzaj_Autora from ewaluacja_liczba_n.models import IloscUdzialowDlaAutoraZaRok @@ -25,13 +26,13 @@ def get_context_data(self, **kwargs): uczelnia = Uczelnia.objects.get_for_request(self.request) ad_qs = Autor_Dyscyplina.objects.filter( - rok__gte=2022, - rok__lte=2025, + rok__gte=OKRES_DOMYSLNY[0], + rok__lte=OKRES_DOMYSLNY[1], autor__aktualna_jednostka__uczelnia=uczelnia, autor__aktualna_jednostka__skupia_pracownikow=True, ) - # 1. Total by rodzaj_pracownika for 2022-2025 + # 1. Total by rodzaj_pracownika dla całego okresu ewaluacji context["rodzaje_pracownika"] = ( ad_qs.values("rodzaj_autora") .annotate(liczba=Count("id")) @@ -182,47 +183,55 @@ def get_context_data(self, **kwargs): # Generate DjangoQL queries and admin filter URLs # DjangoQL needs to reference the related object now + # + # Wspólny prefiks każdego zapytania — ograniczenie do okresu ewaluacji. + # Składany z ``OKRES_DOMYSLNY``, bo inaczej po przestawieniu okresu + # linki do admina po cichu filtrowałyby po poprzednich latach. + okres_ql = f"rok >= {OKRES_DOMYSLNY[0]} and rok <= {OKRES_DOMYSLNY[1]}" context["djangoql_queries"] = { "bez_wymiaru": ( - "rok >= 2022 and rok <= 2025 and " - "(wymiar_etatu = None or wymiar_etatu = 0)" + f"{okres_ql} and (wymiar_etatu = None or wymiar_etatu = 0)" ), "bez_procent_n_sloty": ( - "rok >= 2022 and rok <= 2025 and " + f"{okres_ql} and " "(rodzaj_autora.jest_w_n = True or rodzaj_autora.licz_sloty = True) " "and (procent_dyscypliny = None or procent_dyscypliny = 0 or " "(subdyscyplina_naukowa != None and " "(procent_subdyscypliny = None or procent_subdyscypliny = 0)))" ), "bez_procent_dowolny": ( - "rok >= 2022 and rok <= 2025 and " + f"{okres_ql} and " "(procent_dyscypliny = None or procent_dyscypliny = 0 or " "(subdyscyplina_naukowa != None and " "(procent_subdyscypliny = None or procent_subdyscypliny = 0)))" ), - "rodzaj_n": 'rok >= 2022 and rok <= 2025 and rodzaj_autora.skrot = "N"', - "rodzaj_d": 'rok >= 2022 and rok <= 2025 and rodzaj_autora.skrot = "D"', - "rodzaj_b": 'rok >= 2022 and rok <= 2025 and rodzaj_autora.skrot = "B"', - "rodzaj_z": 'rok >= 2022 and rok <= 2025 and rodzaj_autora.skrot = "Z"', - "brak_danych": "rok >= 2022 and rok <= 2025 and rodzaj_autora = None", + "rodzaj_n": f'{okres_ql} and rodzaj_autora.skrot = "N"', + "rodzaj_d": f'{okres_ql} and rodzaj_autora.skrot = "D"', + "rodzaj_b": f'{okres_ql} and rodzaj_autora.skrot = "B"', + "rodzaj_z": f'{okres_ql} and rodzaj_autora.skrot = "Z"', + "brak_danych": f"{okres_ql} and rodzaj_autora = None", } # URL for custom filter (suma != 100%) - uses custom admin filter instead of DjangoQL context["zla_suma_url"] = ( - "suma_procent=nieprawidlowa&rok__gte=2022&rok__lte=2025" + "suma_procent=nieprawidlowa" + f"&rok__gte={OKRES_DOMYSLNY[0]}&rok__lte={OKRES_DOMYSLNY[1]}" ) # 5. Autorzy z obiema dyscyplinami nie-raportowanymi - # Oblicz nie-raportowane dyscypliny na podstawie sumy udziałów w 2025 - # (suma < 12); reuse the uczelnia already fetched above. - sumy_2025 = ( - IloscUdzialowDlaAutoraZaRok.objects.filter(uczelnia=uczelnia, rok=2025) + # Oblicz nie-raportowane dyscypliny na podstawie sumy udziałów + # w OSTATNIM roku okresu (suma < 12); reuse the uczelnia already + # fetched above. + sumy_ostatni_rok = ( + IloscUdzialowDlaAutoraZaRok.objects.filter( + uczelnia=uczelnia, rok=OKRES_DOMYSLNY[1] + ) .values("dyscyplina_naukowa_id") .annotate(suma=Sum("ilosc_udzialow")) ) nieraportowane_ids = { item["dyscyplina_naukowa_id"] - for item in sumy_2025 + for item in sumy_ostatni_rok if item["suma"] is not None and item["suma"] < 12 } @@ -262,8 +271,8 @@ def post(self, request): uczelnia = Uczelnia.objects.get_for_request(request) updated = ( Autor_Dyscyplina.objects.filter( - rok__gte=2022, - rok__lte=2025, + rok__gte=OKRES_DOMYSLNY[0], + rok__lte=OKRES_DOMYSLNY[1], autor__aktualna_jednostka__uczelnia=uczelnia, autor__aktualna_jednostka__skupia_pracownikow=True, ) @@ -288,8 +297,8 @@ def post(self, request): # Only update records without subdyscyplina (single discipline) updated = ( Autor_Dyscyplina.objects.filter( - rok__gte=2022, - rok__lte=2025, + rok__gte=OKRES_DOMYSLNY[0], + rok__lte=OKRES_DOMYSLNY[1], autor__aktualna_jednostka__uczelnia=uczelnia, autor__aktualna_jednostka__skupia_pracownikow=True, ) @@ -319,8 +328,8 @@ def post(self, request): # Only update records without subdyscyplina (single discipline) updated = ( Autor_Dyscyplina.objects.filter( - rok__gte=2022, - rok__lte=2025, + rok__gte=OKRES_DOMYSLNY[0], + rok__lte=OKRES_DOMYSLNY[1], autor__aktualna_jednostka__uczelnia=uczelnia, autor__aktualna_jednostka__skupia_pracownikow=True, ) @@ -367,8 +376,8 @@ def post(self, request): # Update records without rodzaj_autora or with unknown rodzaj_autora updated = ( Autor_Dyscyplina.objects.filter( - rok__gte=2022, - rok__lte=2025, + rok__gte=OKRES_DOMYSLNY[0], + rok__lte=OKRES_DOMYSLNY[1], autor__aktualna_jednostka__uczelnia=uczelnia, autor__aktualna_jednostka__skupia_pracownikow=True, ) diff --git a/src/ewaluacja_metryki/management/commands/oblicz_metryki.py b/src/ewaluacja_metryki/management/commands/oblicz_metryki.py index 72401c9c5..e3a79a409 100644 --- a/src/ewaluacja_metryki/management/commands/oblicz_metryki.py +++ b/src/ewaluacja_metryki/management/commands/oblicz_metryki.py @@ -5,8 +5,9 @@ from bpp.models import Uczelnia from bpp.util import zaloguj_polkniety_wyjatek +from ewaluacja_common.const import OKRES_DOMYSLNY from ewaluacja_liczba_n.models import IloscUdzialowDlaAutoraZaCalosc -from ewaluacja_liczba_n.utils import oblicz_liczby_n_dla_ewaluacji_2022_2025 +from ewaluacja_liczba_n.utils import oblicz_liczby_n_dla_okresu from ewaluacja_metryki.utils import generuj_metryki logger = logging.getLogger(__name__) @@ -28,14 +29,14 @@ def add_arguments(self, parser): parser.add_argument( "--rok-min", type=int, - default=2022, - help="Początkowy rok okresu ewaluacji (domyślnie 2022)", + default=OKRES_DOMYSLNY[0], + help=f"Początkowy rok okresu ewaluacji (domyślnie {OKRES_DOMYSLNY[0]})", ) parser.add_argument( "--rok-max", type=int, - default=2025, - help="Końcowy rok okresu ewaluacji (domyślnie 2025)", + default=OKRES_DOMYSLNY[1], + help=f"Końcowy rok okresu ewaluacji (domyślnie {OKRES_DOMYSLNY[1]})", ) parser.add_argument( "--minimalny-pk", @@ -95,7 +96,7 @@ def handle(self, *args, **options): self.style.WARNING("Krok 1/2: Przeliczanie liczby N dla uczelni...") ) try: - oblicz_liczby_n_dla_ewaluacji_2022_2025(uczelnia=uczelnia) + oblicz_liczby_n_dla_okresu(uczelnia=uczelnia) self.stdout.write( self.style.SUCCESS("✓ Przeliczono liczby N pomyślnie") ) diff --git a/src/ewaluacja_metryki/models.py b/src/ewaluacja_metryki/models.py index 4361d1d2e..b12a37eea 100644 --- a/src/ewaluacja_metryki/models.py +++ b/src/ewaluacja_metryki/models.py @@ -4,6 +4,7 @@ from bpp.models.autor import Autor from bpp.models.dyscyplina_naukowa import Dyscyplina_Naukowa from bpp.models.jednostka import Jednostka +from ewaluacja_common.const import OKRES_DOMYSLNY class MetrykaAutora(models.Model): @@ -112,11 +113,11 @@ class MetrykaAutora(models.Model): ) rok_min = models.IntegerField( - default=2022, help_text="Początkowy rok okresu ewaluacji" + default=OKRES_DOMYSLNY[0], help_text="Początkowy rok okresu ewaluacji" ) rok_max = models.IntegerField( - default=2025, help_text="Końcowy rok okresu ewaluacji" + default=OKRES_DOMYSLNY[1], help_text="Końcowy rok okresu ewaluacji" ) rodzaj_autora = models.CharField( diff --git a/src/ewaluacja_metryki/tasks.py b/src/ewaluacja_metryki/tasks.py index b5e365591..9455d2eab 100644 --- a/src/ewaluacja_metryki/tasks.py +++ b/src/ewaluacja_metryki/tasks.py @@ -6,7 +6,8 @@ from django.utils import timezone from bpp.models import Uczelnia -from ewaluacja_liczba_n.utils import oblicz_liczby_n_dla_ewaluacji_2022_2025 +from ewaluacja_common.const import OKRES_DOMYSLNY +from ewaluacja_liczba_n.utils import oblicz_liczby_n_dla_okresu from .models import StatusGenerowania from .utils import generuj_metryki @@ -34,8 +35,8 @@ def _resolve_uczelnia(uczelnia_id): def oblicz_metryki_dla_autora_task( self, ilosc_udzialow_id, - rok_min=2022, - rok_max=2025, + rok_min=OKRES_DOMYSLNY[0], + rok_max=OKRES_DOMYSLNY[1], minimalny_pk=0.01, rodzaje_autora=None, uczelnia_id=None, @@ -192,8 +193,8 @@ def finalizuj_generowanie_metryk(results, uczelnia_id=None): @shared_task(bind=True) def generuj_metryki_task_parallel( self, - rok_min=2022, - rok_max=2025, + rok_min=OKRES_DOMYSLNY[0], + rok_max=OKRES_DOMYSLNY[1], minimalny_pk=0.01, nadpisz=True, przelicz_liczbe_n=True, @@ -237,7 +238,7 @@ def generuj_metryki_task_parallel( status.ostatni_komunikat = "Przeliczanie liczby N..." status.save() - oblicz_liczby_n_dla_ewaluacji_2022_2025(uczelnia=uczelnia) + oblicz_liczby_n_dla_okresu(uczelnia=uczelnia) logger.info("Przeliczono liczby N pomyślnie") # Krok 2: Pobierz wszystkie IDs autorów-dyscyplin do przetworzenia @@ -328,8 +329,8 @@ def generuj_metryki_task_parallel( @shared_task(bind=True) def generuj_metryki_task( self, - rok_min=2022, - rok_max=2025, + rok_min=OKRES_DOMYSLNY[0], + rok_max=OKRES_DOMYSLNY[1], minimalny_pk=0.01, nadpisz=True, przelicz_liczbe_n=True, @@ -373,7 +374,7 @@ def generuj_metryki_task( status.ostatni_komunikat = "Przeliczanie liczby N..." status.save() - oblicz_liczby_n_dla_ewaluacji_2022_2025(uczelnia=uczelnia) + oblicz_liczby_n_dla_okresu(uczelnia=uczelnia) logger.info("Przeliczono liczby N pomyślnie") # Krok 2: Oblicz metryki używając wspólnej funkcji diff --git a/src/ewaluacja_metryki/templates/ewaluacja_metryki/lista.html b/src/ewaluacja_metryki/templates/ewaluacja_metryki/lista.html index c96a2616e..3e5bd531e 100644 --- a/src/ewaluacja_metryki/templates/ewaluacja_metryki/lista.html +++ b/src/ewaluacja_metryki/templates/ewaluacja_metryki/lista.html @@ -844,15 +844,18 @@

Uruchom generowanie metryk

hx-trigger="submit"> {% csrf_token %} + {# Domyślne lata pochodzą z ewaluacja_common.const.OKRES_DOMYSLNY, #} + {# podstawia je MetrykiListView._get_status_context. #} + {# NIE zaszywaj tu lat na sztywno. #}
diff --git a/src/ewaluacja_metryki/tests/test_okres_w_formularzu.py b/src/ewaluacja_metryki/tests/test_okres_w_formularzu.py new file mode 100644 index 000000000..62eaf0909 --- /dev/null +++ b/src/ewaluacja_metryki/tests/test_okres_w_formularzu.py @@ -0,0 +1,55 @@ +"""Domyślne lata w formularzu generowania metryk pochodzą z kontekstu widoku. + +Wartości 2022/2025 są tu wpisane JAWNIE (a nie brane z ``OKRES_DOMYSLNY``) +celowo: test ma wykryć przypadkową zmianę okresu ewaluacyjnego, a nie +przyklepać dowolną wartość, którą akurat ma stała. +""" + +import pytest +from django.urls import reverse +from model_bakery import baker + +from bpp.const import GR_WPROWADZANIE_DANYCH +from bpp.models import Dyscyplina_Naukowa, Uczelnia +from ewaluacja_metryki.models import MetrykaAutora + + +@pytest.fixture +def klient_z_metryka(admin_user, db): + uczelnia = baker.make(Uczelnia) + baker.make( + MetrykaAutora, + autor=baker.make("bpp.Autor"), + dyscyplina_naukowa=baker.make(Dyscyplina_Naukowa, nazwa="Test Discipline"), + jednostka=baker.make("bpp.Jednostka"), + uczelnia=uczelnia, + slot_maksymalny=4.0, + slot_nazbierany=2.0, + punkty_nazbierane=100.0, + slot_wszystkie=3.0, + punkty_wszystkie=150.0, + ) + + from django.contrib.auth.models import Group + from django.test import Client + + group, _ = Group.objects.get_or_create(name=GR_WPROWADZANIE_DANYCH) + admin_user.groups.add(group) + + client = Client() + client.force_login(admin_user) + return client + + +def test_formularz_generowania_ma_domyslne_lata_okresu(klient_z_metryka): + response = klient_z_metryka.get(reverse("ewaluacja_metryki:lista")) + assert response.status_code == 200 + + assert response.context["domyslny_rok_min"] == 2022 + assert response.context["domyslny_rok_max"] == 2025 + + # Bez tego szablon mógłby renderować puste ``value=""`` (literówka w + # nazwie zmiennej kontekstu) i formularz POST-owałby pusty rok. + html = response.content.decode() + assert 'name="rok_min" value="2022"' in html + assert 'name="rok_max" value="2025"' in html diff --git a/src/ewaluacja_metryki/tests/test_tasks.py b/src/ewaluacja_metryki/tests/test_tasks.py index bb5d9f136..4c08b7786 100644 --- a/src/ewaluacja_metryki/tests/test_tasks.py +++ b/src/ewaluacja_metryki/tests/test_tasks.py @@ -12,23 +12,23 @@ def test_generowanie_wywoluje_obliczanie_liczby_n(): # 1. Sprawdź import w tasks.py from ewaluacja_metryki import tasks - assert hasattr(tasks, "oblicz_liczby_n_dla_ewaluacji_2022_2025") + assert hasattr(tasks, "oblicz_liczby_n_dla_okresu") # 2. Sprawdź import w management command from ewaluacja_metryki.management.commands import oblicz_metryki - assert hasattr(oblicz_metryki, "oblicz_liczby_n_dla_ewaluacji_2022_2025") + assert hasattr(oblicz_metryki, "oblicz_liczby_n_dla_okresu") # 3. Sprawdź, że funkcja jest używana w kodzie tasks.py import inspect source = inspect.getsource(tasks.generuj_metryki_task) - assert "oblicz_liczby_n_dla_ewaluacji_2022_2025" in source + assert "oblicz_liczby_n_dla_okresu" in source assert "przelicz_liczbe_n" in source # 4. Sprawdź, że management command używa funkcji source_cmd = inspect.getsource(oblicz_metryki.Command.handle) - assert "oblicz_liczby_n_dla_ewaluacji_2022_2025" in source_cmd + assert "oblicz_liczby_n_dla_okresu" in source_cmd assert "bez_liczby_n" in source_cmd @@ -227,7 +227,7 @@ def test_generuj_metryki_task_parallel_uruchamia_chord(uczelnia): from ewaluacja_metryki.tasks import generuj_metryki_task_parallel # Mock oblicz_liczby_n żeby nie wykonywać prawdziwych obliczeń - with patch("ewaluacja_metryki.tasks.oblicz_liczby_n_dla_ewaluacji_2022_2025"): + with patch("ewaluacja_metryki.tasks.oblicz_liczby_n_dla_okresu"): # Mock IloscUdzialowDlaAutoraZaCalosc - patchuj w źródłowym module with patch( "ewaluacja_liczba_n.models.IloscUdzialowDlaAutoraZaCalosc" diff --git a/src/ewaluacja_metryki/utils.py b/src/ewaluacja_metryki/utils.py index 5886a1d33..490a30972 100644 --- a/src/ewaluacja_metryki/utils.py +++ b/src/ewaluacja_metryki/utils.py @@ -6,6 +6,7 @@ from bpp.models import Autor_Dyscyplina from bpp.util import zaloguj_polkniety_wyjatek +from ewaluacja_common.const import OKRES_DOMYSLNY from .models import MetrykaAutora @@ -34,8 +35,8 @@ def oblicz_metryki_dla_autora( autor, dyscyplina, uczelnia, - rok_min=2022, - rok_max=2025, + rok_min=OKRES_DOMYSLNY[0], + rok_max=OKRES_DOMYSLNY[1], minimalny_pk=Decimal("0.01"), slot_maksymalny=None, ): @@ -203,7 +204,9 @@ def oblicz_metryki_dla_autora( return metryka, created -def przelicz_metryki_dla_publikacji(publikacja, rok_min=2022, rok_max=2025): +def przelicz_metryki_dla_publikacji( + publikacja, rok_min=OKRES_DOMYSLNY[0], rok_max=OKRES_DOMYSLNY[1] +): """ Przelicza metryki dla wszystkich autorów danej publikacji z przypisanymi dyscyplinami. @@ -294,7 +297,13 @@ def _get_ilosc_udzialow_queryset(ilosc_udzialow_queryset, uczelnia=None): return qs -def _should_skip_author(autor, dyscyplina, rodzaje_autora, rok_min=2022, rok_max=2025): +def _should_skip_author( + autor, + dyscyplina, + rodzaje_autora, + rok_min=OKRES_DOMYSLNY[0], + rok_max=OKRES_DOMYSLNY[1], +): """ Sprawdza czy autor powinien być pominięty na podstawie rodzaju_autora. @@ -305,8 +314,8 @@ def _should_skip_author(autor, dyscyplina, rodzaje_autora, rok_min=2022, rok_max autor: Obiekt Autor dyscyplina: Obiekt Dyscyplina_Naukowa rodzaje_autora: Lista akceptowalnych skrótów rodzajów autorów - rok_min: Początkowy rok okresu ewaluacji (domyślnie 2022) - rok_max: Końcowy rok okresu ewaluacji (domyślnie 2025) + rok_min: Początkowy rok okresu ewaluacji (domyślnie z OKRES_DOMYSLNY) + rok_max: Końcowy rok okresu ewaluacji (domyślnie z OKRES_DOMYSLNY) Returns: Tuple (bool, Autor_Dyscyplina): (czy_pominąć, najnowszy_rekord_w_okresie) @@ -547,8 +556,8 @@ def _process_single_author( def generuj_metryki( - rok_min=2022, - rok_max=2025, + rok_min=OKRES_DOMYSLNY[0], + rok_max=OKRES_DOMYSLNY[1], minimalny_pk=Decimal("0.01"), nadpisz=True, rodzaje_autora=None, diff --git a/src/ewaluacja_metryki/views/detail.py b/src/ewaluacja_metryki/views/detail.py index 3462e4cfb..c18c2c49a 100644 --- a/src/ewaluacja_metryki/views/detail.py +++ b/src/ewaluacja_metryki/views/detail.py @@ -2,6 +2,7 @@ from django.shortcuts import redirect from django.views.generic import DetailView +from ewaluacja_common.const import lata_okresu from ewaluacja_common.models import Rodzaj_Autora from raport_slotow.uczelnia_helper import uczelnia_dla_odczytu @@ -87,7 +88,7 @@ def _get_discipline_years_context(self, metryka): Autor_Dyscyplina.objects.filter( autor=metryka.autor, dyscyplina_naukowa=metryka.dyscyplina_naukowa, - rok__in=[2022, 2023, 2024, 2025], + rok__in=lata_okresu(), ) .select_related("dyscyplina_naukowa", "subdyscyplina_naukowa") .order_by("rok") diff --git a/src/ewaluacja_metryki/views/generation.py b/src/ewaluacja_metryki/views/generation.py index c9c9444ef..2e9923b70 100644 --- a/src/ewaluacja_metryki/views/generation.py +++ b/src/ewaluacja_metryki/views/generation.py @@ -5,6 +5,7 @@ from django.utils.decorators import method_decorator from django.views import View +from ewaluacja_common.const import OKRES_DOMYSLNY from ewaluacja_common.models import Rodzaj_Autora from raport_slotow.uczelnia_helper import uczelnia_dla_odczytu @@ -55,8 +56,8 @@ def post(self, request, *args, **kwargs): return redirect("ewaluacja_metryki:lista") # Pobierz parametry z formularza (jeśli są) - rok_min = int(request.POST.get("rok_min", 2022)) - rok_max = int(request.POST.get("rok_max", 2025)) + rok_min = int(request.POST.get("rok_min", OKRES_DOMYSLNY[0])) + rok_max = int(request.POST.get("rok_max", OKRES_DOMYSLNY[1])) minimalny_pk = float(request.POST.get("minimalny_pk", 0.01)) nadpisz = request.POST.get("nadpisz", "on") == "on" diff --git a/src/ewaluacja_metryki/views/list.py b/src/ewaluacja_metryki/views/list.py index e98341408..66f496cc6 100644 --- a/src/ewaluacja_metryki/views/list.py +++ b/src/ewaluacja_metryki/views/list.py @@ -2,6 +2,7 @@ from django.views.generic import ListView from bpp.models import Jednostka +from ewaluacja_common.const import OKRES_DOMYSLNY from ewaluacja_common.models import Rodzaj_Autora from raport_slotow.uczelnia_helper import uczelnia_dla_odczytu @@ -280,6 +281,10 @@ def _get_status_context(self): "dostepne_rodzaje_autorow": Rodzaj_Autora.objects.filter( licz_sloty=True ).order_by("sort"), + # Wartości domyślne pól "Rok początkowy"/"Rok końcowy" w modalu + # generowania — z jednego źródła prawdy, nie zaszyte w HTML-u. + "domyslny_rok_min": OKRES_DOMYSLNY[0], + "domyslny_rok_max": OKRES_DOMYSLNY[1], } # Oblicz procent postępu diff --git a/src/ewaluacja_optymalizacja/core/data_loader.py b/src/ewaluacja_optymalizacja/core/data_loader.py index 02494225c..b21492331 100644 --- a/src/ewaluacja_optymalizacja/core/data_loader.py +++ b/src/ewaluacja_optymalizacja/core/data_loader.py @@ -12,6 +12,7 @@ from bpp import const from bpp.models import Cache_Punktacja_Autora_Query, Dyscyplina_Naukowa +from ewaluacja_common.const import OKRES_DOMYSLNY from ewaluacja_liczba_n.models import IloscUdzialowDlaAutoraZaCalosc from .data_structures import Pub @@ -36,12 +37,12 @@ def generate_pub_data(dyscyplina_nazwa: str, verbose: bool = False) -> list[Pub] f"Discipline '{dyscyplina_nazwa}' not found in database" ) from e - # Query cache data for years 2022-2025 and given discipline + # Query cache data for the current evaluation period and given discipline cache_entries = ( Cache_Punktacja_Autora_Query.objects.filter( dyscyplina=dyscyplina, - rekord__rok__gte=2022, - rekord__rok__lte=2025, + rekord__rok__gte=OKRES_DOMYSLNY[0], + rekord__rok__lte=OKRES_DOMYSLNY[1], ) .select_related( "autor", diff --git a/src/ewaluacja_optymalizacja/management/commands/reset_disciplines.py b/src/ewaluacja_optymalizacja/management/commands/reset_disciplines.py index eb52bb877..e222a487e 100644 --- a/src/ewaluacja_optymalizacja/management/commands/reset_disciplines.py +++ b/src/ewaluacja_optymalizacja/management/commands/reset_disciplines.py @@ -7,6 +7,7 @@ from bpp.models import Patent_Autor, Wydawnictwo_Ciagle_Autor, Wydawnictwo_Zwarte_Autor from bpp.util import zaloguj_polkniety_wyjatek +from ewaluacja_common.const import OKRES_DOMYSLNY logger = logging.getLogger(__name__) @@ -32,10 +33,16 @@ def add_arguments(self, parser): help="Don't wait for denorm flush after resetting", ) parser.add_argument( - "--year-from", type=int, default=2022, help="Starting year (default: 2022)" + "--year-from", + type=int, + default=OKRES_DOMYSLNY[0], + help=f"Starting year (default: {OKRES_DOMYSLNY[0]})", ) parser.add_argument( - "--year-to", type=int, default=2025, help="Ending year (default: 2025)" + "--year-to", + type=int, + default=OKRES_DOMYSLNY[1], + help=f"Ending year (default: {OKRES_DOMYSLNY[1]})", ) def get_dirty_count(self): diff --git a/src/ewaluacja_optymalizacja/tasks/discipline_swap/analysis.py b/src/ewaluacja_optymalizacja/tasks/discipline_swap/analysis.py index 68a8089b1..820f97895 100644 --- a/src/ewaluacja_optymalizacja/tasks/discipline_swap/analysis.py +++ b/src/ewaluacja_optymalizacja/tasks/discipline_swap/analysis.py @@ -9,6 +9,8 @@ import rollbar +from ewaluacja_common.const import OKRES_DOMYSLNY + from .simulation import simulate_discipline_swap logger = logging.getLogger(__name__) @@ -119,8 +121,8 @@ def partition_works_into_chunks(rekord_ids, chunk_size=50): def _analyze_discipline_swap_impl( # noqa: C901 task, uczelnia_id, - rok_min=2022, - rok_max=2025, + rok_min=OKRES_DOMYSLNY[0], + rok_max=OKRES_DOMYSLNY[1], ): """ Implementacja analizy możliwości zamiany dyscyplin. @@ -137,8 +139,8 @@ def _analyze_discipline_swap_impl( # noqa: C901 Args: task: Celery task object (self) do aktualizacji statusu uczelnia_id: ID uczelni - rok_min: Minimalny rok analizy (domyślnie 2022) - rok_max: Maksymalny rok analizy (domyślnie 2025) + rok_min: Minimalny rok analizy (domyślnie początek OKRES_DOMYSLNY) + rok_max: Maksymalny rok analizy (domyślnie koniec OKRES_DOMYSLNY) Returns: Dictionary z wynikami analizy diff --git a/src/ewaluacja_optymalizacja/tasks/discipline_swap/main_tasks.py b/src/ewaluacja_optymalizacja/tasks/discipline_swap/main_tasks.py index 851c1b844..dbfb225c4 100644 --- a/src/ewaluacja_optymalizacja/tasks/discipline_swap/main_tasks.py +++ b/src/ewaluacja_optymalizacja/tasks/discipline_swap/main_tasks.py @@ -5,6 +5,8 @@ from celery import shared_task from celery_singleton import Singleton +from ewaluacja_common.const import OKRES_DOMYSLNY + from .analysis import _analyze_discipline_swap_impl logger = logging.getLogger(__name__) @@ -20,8 +22,8 @@ def analyze_discipline_swap_task( self, uczelnia_id, - rok_min=2022, - rok_max=2025, + rok_min=OKRES_DOMYSLNY[0], + rok_max=OKRES_DOMYSLNY[1], ): """ Analizuje publikacje pod kątem możliwości zamiany dyscyplin. @@ -33,8 +35,8 @@ def analyze_discipline_swap_task( Args: uczelnia_id: ID uczelni - rok_min: Minimalny rok analizy (domyślnie 2022) - rok_max: Maksymalny rok analizy (domyślnie 2025) + rok_min: Minimalny rok analizy (domyślnie początek OKRES_DOMYSLNY) + rok_max: Maksymalny rok analizy (domyślnie koniec OKRES_DOMYSLNY) Returns: Dictionary z wynikami analizy diff --git a/src/ewaluacja_optymalizacja/tasks/helpers.py b/src/ewaluacja_optymalizacja/tasks/helpers.py index aa78d28b9..a70193205 100644 --- a/src/ewaluacja_optymalizacja/tasks/helpers.py +++ b/src/ewaluacja_optymalizacja/tasks/helpers.py @@ -2,6 +2,8 @@ import logging +from ewaluacja_common.const import OKRES_DOMYSLNY + logger = logging.getLogger(__name__) @@ -70,7 +72,8 @@ def _create_optimization_snapshot(uczelnia, logger_func): def _collect_ids_to_unpin(uczelnia, autorzy_z_wynikami, logger_func): """Collect IDs of publications to unpin. - Only considers publications from years 2022-2025 (rok >= 2022 and rok < 2026). + Only considers publications from the current evaluation period + (``OKRES_DOMYSLNY``, przedział domknięty). Unpins publications that are either: - Not in optimization results, OR - In optimization results but with slot < 1.0 @@ -117,12 +120,12 @@ def _collect_ids_to_unpin(uczelnia, autorzy_z_wynikami, logger_func): ).values_list("rekord_id", "slots") } - # Wydawnictwa ciągłe - only years 2022-2025 + # Wydawnictwa ciągłe - tylko lata okresu ewaluacji for udział in Wydawnictwo_Ciagle_Autor.objects.filter( autor_id=autor_id, przypieta=True, - rekord__rok__gte=2022, - rekord__rok__lt=2026, + rekord__rok__gte=OKRES_DOMYSLNY[0], + rekord__rok__lte=OKRES_DOMYSLNY[1], ).select_related("rekord"): rekord_id = (ct_ciagle.pk, udział.rekord.pk) @@ -135,12 +138,12 @@ def _collect_ids_to_unpin(uczelnia, autorzy_z_wynikami, logger_func): ): ids_to_unpin_ciagle.append(udział.pk) - # Wydawnictwa zwarte - only years 2022-2025 + # Wydawnictwa zwarte - tylko lata okresu ewaluacji for udział in Wydawnictwo_Zwarte_Autor.objects.filter( autor_id=autor_id, przypieta=True, - rekord__rok__gte=2022, - rekord__rok__lt=2026, + rekord__rok__gte=OKRES_DOMYSLNY[0], + rekord__rok__lte=OKRES_DOMYSLNY[1], ).select_related("rekord"): rekord_id = (ct_zwarte.pk, udział.rekord.pk) diff --git a/src/ewaluacja_optymalizacja/tasks/reset_pins.py b/src/ewaluacja_optymalizacja/tasks/reset_pins.py index 910740483..41d18107e 100644 --- a/src/ewaluacja_optymalizacja/tasks/reset_pins.py +++ b/src/ewaluacja_optymalizacja/tasks/reset_pins.py @@ -6,6 +6,7 @@ from celery import shared_task from celery_singleton import Singleton +from ewaluacja_common.const import OKRES_DOMYSLNY from ewaluacja_liczba_n.models import LiczbaNDlaUczelni from .helpers import _wait_for_denorm @@ -15,7 +16,7 @@ def _reset_pins_for_authors(autorzy_ids, task, snapshot_pk, logger_func): - """Reset pins for all authors in years 2022-2025.""" + """Reset pins for all authors within the current evaluation period.""" from django.db.models import Q from bpp.models import ( @@ -25,7 +26,9 @@ def _reset_pins_for_authors(autorzy_ids, task, snapshot_pk, logger_func): ) base_filter = Q( - rekord__rok__gte=2022, rekord__rok__lte=2025, autor_id__in=autorzy_ids + rekord__rok__gte=OKRES_DOMYSLNY[0], + rekord__rok__lte=OKRES_DOMYSLNY[1], + autor_id__in=autorzy_ids, ) updated_count = 0 @@ -110,7 +113,7 @@ def _reset_pins_for_authors(autorzy_ids, task, snapshot_pk, logger_func): def reset_discipline_pins_task( self, uczelnia_id, dyscyplina_id, owner_id=None, algorithm_mode="two-phase" ): - """Reset przypięć JEDNEJ dyscypliny (2022-2025) + optymalizacja — w tle. + """Reset przypięć JEDNEJ dyscypliny (okres ewaluacji) + optymalizacja — w tle. Wydzielone z widoku ``reset_discipline_pins``: wcześniej request wisiał do 10 minut, śpiąc w pętli i odpytując globalne ``DirtyInstance.count()`` @@ -148,7 +151,9 @@ def reset_discipline_pins_task( autorzy_ids = set( Autor_Dyscyplina.objects.filter( - rok__gte=2022, rok__lte=2025, dyscyplina_naukowa=dyscyplina + rok__gte=OKRES_DOMYSLNY[0], + rok__lte=OKRES_DOMYSLNY[1], + dyscyplina_naukowa=dyscyplina, ) .values_list("autor_id", flat=True) .distinct() @@ -171,8 +176,8 @@ def reset_discipline_pins_task( ) base_filter = Q( - rekord__rok__gte=2022, - rekord__rok__lte=2025, + rekord__rok__gte=OKRES_DOMYSLNY[0], + rekord__rok__lte=OKRES_DOMYSLNY[1], dyscyplina_naukowa=dyscyplina, autor_id__in=autorzy_ids, ) @@ -252,7 +257,8 @@ def reset_discipline_pins_task( ) def reset_all_pins_task(self, uczelnia_id, algorithm_mode="two-phase"): """ - Resetuje przypięcia dla wszystkich rekordów 2022-2025 gdzie autor ma dyscyplinę, + Resetuje przypięcia dla wszystkich rekordów okresu ewaluacji, gdzie autor ma + dyscyplinę, jest zatrudniony i afiliuje. Args: @@ -277,10 +283,12 @@ def reset_all_pins_task(self, uczelnia_id, algorithm_mode="two-phase"): # Update task state self.update_state(state="PROGRESS", meta={"step": "collecting", "progress": 10}) - # Pobierz wszystkich autorów którzy mają Autor_Dyscyplina w latach 2022-2025 + # Pobierz wszystkich autorów którzy mają Autor_Dyscyplina w okresie ewaluacji # dla dowolnej dyscypliny autorzy_ids = set( - Autor_Dyscyplina.objects.filter(rok__gte=2022, rok__lte=2025) + Autor_Dyscyplina.objects.filter( + rok__gte=OKRES_DOMYSLNY[0], rok__lte=OKRES_DOMYSLNY[1] + ) .values_list("autor_id", flat=True) .distinct() ) diff --git a/src/ewaluacja_optymalizacja/tasks/unpinning/capacity_analysis.py b/src/ewaluacja_optymalizacja/tasks/unpinning/capacity_analysis.py index ae08adb0e..ac8502830 100644 --- a/src/ewaluacja_optymalizacja/tasks/unpinning/capacity_analysis.py +++ b/src/ewaluacja_optymalizacja/tasks/unpinning/capacity_analysis.py @@ -15,6 +15,8 @@ from django.db import transaction +from ewaluacja_common.const import OKRES_DOMYSLNY + if TYPE_CHECKING: from bpp.models import Dyscyplina_Naukowa @@ -75,8 +77,8 @@ def calculate_author_slot_usage( current_slots_agg = Cache_Punktacja_Autora_Query.objects.filter( autor_id=author_id, dyscyplina=dyscyplina, - rekord__rok__gte=2022, - rekord__rok__lte=2025, + rekord__rok__gte=OKRES_DOMYSLNY[0], + rekord__rok__lte=OKRES_DOMYSLNY[1], ).aggregate(total=Sum("slot")) current_slots = ( @@ -130,8 +132,8 @@ def identify_unpinning_candidates( # noqa: C901 cache_entries = ( Cache_Punktacja_Autora_Query.objects.filter( dyscyplina=dyscyplina, - rekord__rok__gte=2022, - rekord__rok__lte=2025, + rekord__rok__gte=OKRES_DOMYSLNY[0], + rekord__rok__lte=OKRES_DOMYSLNY[1], ) .select_related("autor", "rekord") .exclude(pkdaut=0) diff --git a/src/ewaluacja_optymalizacja/tests/test_okres_w_zapytaniach.py b/src/ewaluacja_optymalizacja/tests/test_okres_w_zapytaniach.py new file mode 100644 index 000000000..0a000d13b --- /dev/null +++ b/src/ewaluacja_optymalizacja/tests/test_okres_w_zapytaniach.py @@ -0,0 +1,34 @@ +"""Strażnik: listy lat (``rok__in``) też muszą wynikać z ``OKRES_DOMYSLNY``. + +Druga — obok filtrów ``rok__gte`` / ``rok__lte`` — kategoria literałów, które +po zmianie okresu liczyłyby po cichu stare lata: wypisane wprost listy +``[2022, 2023, 2024, 2025]`` i ``range(2022, 2026)``. Dochodzi tu pułapka +o jeden: okres to przedział DOMKNIĘTY, a ``range`` ma prawy koniec wyłączny. +""" + +from unittest import mock + +from ewaluacja_common.const import lata_okresu +from ewaluacja_optymalizacja.views.evaluation_browser.filters import _build_base_filter + +OKRES_TESTOWY = (2026, 2029) + + +def test_lata_okresu_zwraca_przedzial_domkniety(): + # Ostatni rok okresu MUSI się załapać — to jest ten błąd o jeden. + assert lata_okresu((2022, 2025)) == [2022, 2023, 2024, 2025] + assert lata_okresu((2026, 2030)) == [2026, 2027, 2028, 2029, 2030] + + +def test_lata_okresu_czyta_okres_domyslny_przy_wywolaniu(): + assert lata_okresu() == [2022, 2023, 2024, 2025] + + with mock.patch("ewaluacja_common.const.OKRES_DOMYSLNY", OKRES_TESTOWY): + assert lata_okresu() == [2026, 2027, 2028, 2029] + + +def test_filtr_przegladarki_publikacji_bierze_lata_z_okresu_domyslnego(): + assert _build_base_filter({})["rok__in"] == [2022, 2023, 2024, 2025] + + with mock.patch("ewaluacja_common.const.OKRES_DOMYSLNY", OKRES_TESTOWY): + assert _build_base_filter({})["rok__in"] == [2026, 2027, 2028, 2029] diff --git a/src/ewaluacja_optymalizacja/utils.py b/src/ewaluacja_optymalizacja/utils.py index 9d3417334..029b124d3 100644 --- a/src/ewaluacja_optymalizacja/utils.py +++ b/src/ewaluacja_optymalizacja/utils.py @@ -17,6 +17,7 @@ Wydawnictwo_Zwarte_Autor, ) from bpp.models.sloty.core import IPunktacjaCacher +from ewaluacja_common.const import OKRES_DOMYSLNY def wersje_dyscyplin( @@ -294,9 +295,13 @@ def close(self): self.pbar.close() -def wszystkie_wersje_rekordow( - rok_min=2022, - rok_max=2025, +# noqa C901: złożoność 11 > 10 istniała PRZED tym refaktorem (zmieniono tu +# wyłącznie wartości domyślne argumentów). Wyciszamy, bo dotknięcie pliku +# wciąga go do CI-owego "Lint changed files"; rozplątanie tej funkcji to +# osobna zmiana, nie część refaktoru okresu ewaluacyjnego. +def wszystkie_wersje_rekordow( # noqa: C901 + rok_min=OKRES_DOMYSLNY[0], + rok_max=OKRES_DOMYSLNY[1], max_workers=None, batch_size=50, use_multiprocessing=True, @@ -305,8 +310,8 @@ def wszystkie_wersje_rekordow( Process all publication records with optional multiprocessing support. Args: - rok_min: Minimum year to process (default: 2022) - rok_max: Maximum year to process (default: 2025) + rok_min: Minimum year to process (default: start of OKRES_DOMYSLNY) + rok_max: Maximum year to process (default: end of OKRES_DOMYSLNY) max_workers: Number of processes to use (default: CPU count) batch_size: Number of records per batch (default: 200) use_multiprocessing: Whether to use multiprocessing (default: True) diff --git a/src/ewaluacja_optymalizacja/views/author_works.py b/src/ewaluacja_optymalizacja/views/author_works.py index aa07e9fad..81d58dbbd 100644 --- a/src/ewaluacja_optymalizacja/views/author_works.py +++ b/src/ewaluacja_optymalizacja/views/author_works.py @@ -9,6 +9,7 @@ from bpp.models.patent import Patent_Autor from bpp.models.wydawnictwo_ciagle import Wydawnictwo_Ciagle_Autor from bpp.models.wydawnictwo_zwarte import Wydawnictwo_Zwarte_Autor +from ewaluacja_common.const import OKRES_DOMYSLNY from ewaluacja_metryki.models import MetrykaAutora from ..models import OptimizationRun @@ -133,11 +134,11 @@ def author_works_detail(request, run_pk, autor_pk): inna_dyscyplina = None inny_run = None - # Get author's disciplines in 2022-2025 + # Get author's disciplines within the current evaluation period autor_dyscypliny = Autor_Dyscyplina.objects.filter( autor=autor, - rok__gte=2022, - rok__lte=2025, + rok__gte=OKRES_DOMYSLNY[0], + rok__lte=OKRES_DOMYSLNY[1], ).select_related("dyscyplina_naukowa", "subdyscyplina_naukowa") # Collect all disciplines for this author diff --git a/src/ewaluacja_optymalizacja/views/evaluation_browser/discipline_summary.py b/src/ewaluacja_optymalizacja/views/evaluation_browser/discipline_summary.py index f63638e2c..80b213f1b 100644 --- a/src/ewaluacja_optymalizacja/views/evaluation_browser/discipline_summary.py +++ b/src/ewaluacja_optymalizacja/views/evaluation_browser/discipline_summary.py @@ -1,6 +1,7 @@ """Helpers do podsumowania dyscyplin i opcji filtrów przeglądarki.""" from bpp.models import Dyscyplina_Naukowa +from ewaluacja_common.const import lata_okresu from ...models import OptimizationRun @@ -133,5 +134,5 @@ def _get_filter_options(uczelnia): return { "dyscypliny": dyscypliny, - "lata": [2022, 2023, 2024, 2025], + "lata": lata_okresu(), } diff --git a/src/ewaluacja_optymalizacja/views/evaluation_browser/filters.py b/src/ewaluacja_optymalizacja/views/evaluation_browser/filters.py index 5ca981cbf..027c883a3 100644 --- a/src/ewaluacja_optymalizacja/views/evaluation_browser/filters.py +++ b/src/ewaluacja_optymalizacja/views/evaluation_browser/filters.py @@ -5,6 +5,7 @@ Wydawnictwo_Ciagle_Autor, Wydawnictwo_Zwarte_Autor, ) +from ewaluacja_common.const import lata_okresu def _apply_dyscyplina_nieprzypisana_filter(ciagle_qs, zwarte_qs, filters): @@ -27,7 +28,7 @@ def _apply_dyscyplina_nieprzypisana_filter(ciagle_qs, zwarte_qs, filters): return ciagle_qs, zwarte_qs dyscyplina_nieprzypisana_id = int(dyscyplina_nieprzypisana) - lata_filtra = [int(rok)] if rok else [2022, 2023, 2024, 2025] + lata_filtra = [int(rok)] if rok else lata_okresu() # Znajdz DWUDYSCYPLINOWCÓW z dana dyscyplina (glowna lub subdyscyplina) # Wymagamy subdyscyplina_naukowa__isnull=False - autor musi mieć dwie dyscypliny @@ -73,7 +74,7 @@ def _build_base_filter(filters): punkty_od = filters.get("punkty_od") punkty_do = filters.get("punkty_do") - base_filter = {"rok__in": [2022, 2023, 2024, 2025]} + base_filter = {"rok__in": lata_okresu()} if rok: base_filter["rok"] = int(rok) diff --git a/src/ewaluacja_optymalizacja/views/exports.py b/src/ewaluacja_optymalizacja/views/exports.py index c2fbb826b..5ea1fc1d2 100644 --- a/src/ewaluacja_optymalizacja/views/exports.py +++ b/src/ewaluacja_optymalizacja/views/exports.py @@ -8,6 +8,8 @@ from django.contrib.auth.decorators import login_required from django.shortcuts import get_object_or_404, redirect +from ewaluacja_common.const import OKRES_DOMYSLNY + from ..models import OptimizationRun logger = logging.getLogger(__name__) @@ -361,12 +363,12 @@ def export_sedn_report_1(request): key = (m["autor_id"], m["dyscyplina_naukowa_id"]) selected_works[key] = set(tuple(x) for x in (m["prace_nazbierane"] or [])) - # Pobierz wszystkie Cache_Punktacja_Autora_Query dla tych dyscyplin (lata 2022-2025) + # Pobierz Cache_Punktacja_Autora_Query dla tych dyscyplin (lata okresu ewaluacji) prace = ( Cache_Punktacja_Autora_Query.objects.filter( dyscyplina_id__in=discipline_ids, - rekord__rok__gte=2022, - rekord__rok__lte=2025, + rekord__rok__gte=OKRES_DOMYSLNY[0], + rekord__rok__lte=OKRES_DOMYSLNY[1], ) .select_related( "rekord", @@ -487,12 +489,12 @@ def export_sedn_report_2(request): } ) - # Pobierz prace z lat 2022-2025 + # Pobierz prace z lat okresu ewaluacji prace = ( Cache_Punktacja_Autora_Query.objects.filter( dyscyplina_id__in=discipline_ids, - rekord__rok__gte=2022, - rekord__rok__lte=2025, + rekord__rok__gte=OKRES_DOMYSLNY[0], + rekord__rok__lte=OKRES_DOMYSLNY[1], ) .select_related( "rekord", diff --git a/src/ewaluacja_optymalizacja/views/helpers.py b/src/ewaluacja_optymalizacja/views/helpers.py index c73cba10e..bf0d87158 100644 --- a/src/ewaluacja_optymalizacja/views/helpers.py +++ b/src/ewaluacja_optymalizacja/views/helpers.py @@ -5,6 +5,8 @@ from django.db.models import Q +from ewaluacja_common.const import OKRES_DOMYSLNY + logger = logging.getLogger(__name__) # Klucze sesji dla śledzenia postępu denormalizacji @@ -134,15 +136,17 @@ def _check_for_problematic_slots(): from bpp.models import Autor_Dyscyplina, Cache_Punktacja_Autora_Query autorzy_z_dyscyplinami = set( - Autor_Dyscyplina.objects.filter(rok__gte=2022, rok__lte=2025) + Autor_Dyscyplina.objects.filter( + rok__gte=OKRES_DOMYSLNY[0], rok__lte=OKRES_DOMYSLNY[1] + ) .values_list("autor_id", flat=True) .distinct() ) return Cache_Punktacja_Autora_Query.objects.filter( slot__lt=Decimal("0.1"), - rekord__rok__gte=2022, - rekord__rok__lte=2025, + rekord__rok__gte=OKRES_DOMYSLNY[0], + rekord__rok__lte=OKRES_DOMYSLNY[1], autor_id__in=autorzy_z_dyscyplinami, ).exists() @@ -223,10 +227,10 @@ def _calculate_liczba_n_stats(run, author_results): def _get_discipline_pin_stats(dyscyplina_naukowa): """ - Oblicza statystyki przypięć/odpięć dla danej dyscypliny w latach 2022-2025. + Oblicza statystyki przypięć/odpięć dla danej dyscypliny w okresie ewaluacji. - Liczy tylko rekordy gdzie autor ma Autor_Dyscyplina w latach 2022-2025 - dla tej dyscypliny. + Liczy tylko rekordy gdzie autor ma Autor_Dyscyplina w latach okresu + ewaluacji dla tej dyscypliny. Args: dyscyplina_naukowa: Obiekt Dyscyplina_Naukowa @@ -242,11 +246,11 @@ def _get_discipline_pin_stats(dyscyplina_naukowa): Wydawnictwo_Zwarte_Autor, ) - # Pobierz autorów którzy mają Autor_Dyscyplina w latach 2022-2025 dla tej dyscypliny + # Pobierz autorów mających Autor_Dyscyplina w okresie ewaluacji dla tej dyscypliny autorzy_ids = set( Autor_Dyscyplina.objects.filter( - rok__gte=2022, - rok__lte=2025, + rok__gte=OKRES_DOMYSLNY[0], + rok__lte=OKRES_DOMYSLNY[1], dyscyplina_naukowa=dyscyplina_naukowa, ) .values_list("autor_id", flat=True) @@ -264,8 +268,8 @@ def _get_discipline_pin_stats(dyscyplina_naukowa): # Filtr bazowy dla wszystkich modeli base_filter = Q( - rekord__rok__gte=2022, - rekord__rok__lte=2025, + rekord__rok__gte=OKRES_DOMYSLNY[0], + rekord__rok__lte=OKRES_DOMYSLNY[1], dyscyplina_naukowa=dyscyplina_naukowa, autor_id__in=autorzy_ids, ) diff --git a/src/ewaluacja_optymalizacja/views/verification.py b/src/ewaluacja_optymalizacja/views/verification.py index d948aa03c..a906c8d94 100644 --- a/src/ewaluacja_optymalizacja/views/verification.py +++ b/src/ewaluacja_optymalizacja/views/verification.py @@ -12,6 +12,7 @@ Wydawnictwo_Zwarte, Wydawnictwo_Zwarte_Autor, ) +from ewaluacja_common.const import OKRES_DOMYSLNY logger = logging.getLogger(__name__) @@ -19,14 +20,16 @@ @login_required def database_verification_view(request): """ - Wyświetla listę prac z autorami mającymi sloty poniżej 0.1 w latach 2022-2025. + Wyświetla prace z autorami mającymi sloty poniżej 0.1 w okresie ewaluacji. Takie sloty należy usunąć przed dalszymi krokami optymalizacji. """ from bpp.models import Autor_Dyscyplina, Cache_Punktacja_Autora_Query - # Pobierz autorów którzy mają przypisane dyscypliny w latach 2022-2025 + # Pobierz autorów którzy mają przypisane dyscypliny w okresie ewaluacji autorzy_z_dyscyplinami = set( - Autor_Dyscyplina.objects.filter(rok__gte=2022, rok__lte=2025) + Autor_Dyscyplina.objects.filter( + rok__gte=OKRES_DOMYSLNY[0], rok__lte=OKRES_DOMYSLNY[1] + ) .values_list("autor_id", flat=True) .distinct() ) @@ -35,8 +38,8 @@ def database_verification_view(request): problematic_records = ( Cache_Punktacja_Autora_Query.objects.filter( slot__lt=Decimal("0.1"), - rekord__rok__gte=2022, - rekord__rok__lte=2025, + rekord__rok__gte=OKRES_DOMYSLNY[0], + rekord__rok__lte=OKRES_DOMYSLNY[1], autor_id__in=autorzy_z_dyscyplinami, ) .select_related("rekord", "autor", "dyscyplina") @@ -59,10 +62,11 @@ def database_verification_view(request): przypieta=False, ).select_related("rekord", "autor", "dyscyplina_naukowa") - # Liczba publikacji z rok >= 2022, gdzie autor ma dyscyplinę, ale brak daty oświadczenia + # Liczba publikacji od początku okresu ewaluacji, gdzie autor ma dyscyplinę, + # ale brak daty oświadczenia brak_oswiadczenia_ciagle_count = ( Wydawnictwo_Ciagle.objects.filter( - rok__gte=2022, + rok__gte=OKRES_DOMYSLNY[0], autorzy_set__dyscyplina_naukowa__isnull=False, autorzy_set__data_oswiadczenia__isnull=True, ) @@ -72,7 +76,7 @@ def database_verification_view(request): brak_oswiadczenia_zwarte_count = ( Wydawnictwo_Zwarte.objects.filter( - rok__gte=2022, + rok__gte=OKRES_DOMYSLNY[0], autorzy_set__dyscyplina_naukowa__isnull=False, autorzy_set__data_oswiadczenia__isnull=True, )