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 @@