From fd8cd85f6a72e01f93d177f8c81c5dc7db5c1027 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 17:32:23 +0200 Subject: [PATCH 01/15] fix(tz): replace naive datetime.now() writes to DateTimeField MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zamiana datetime.now() → timezone.now() w miejscach, gdzie wartość trafiała do pola DateTimeField (zapis lub filtr). Przy USE_TZ=True Django emitowało RuntimeWarning "received a naive datetime" i interpretowało wartość w lokalnej strefie czasowej, co przy DST mogło powodować niespójności dat w bazie. - bpp.util.remove_old_objects (uzywane przez oswiadczenia, integrator2) - bpp.admin.templates.TemplateAdmin.template_updated filtr - ewaluacja_optymalizacja.tasks.optimization OptimizationRun .finished_at (3 miejsca) - ewaluacja_optymalizacja management commands solve_uczelnia, solve_evaluation Testowe literaly "2023-01-01 00:00:00" w pbn_export_queue zamienione na timezone.make_aware(datetime(...)). test_OstatnioZmieniony: datetime.now() -> timezone.now(). Co-Authored-By: Claude Opus 4.7 (1M context) --- src/bpp/admin/templates.py | 5 +++-- .../+naive-datetime-db-writes.bugfix.rst | 18 ++++++++++++++++++ .../test_multiseek/test_multiseek_misc.py | 6 +++--- src/bpp/util.py | 4 ++-- .../management/commands/solve_evaluation.py | 4 ++-- .../management/commands/solve_uczelnia.py | 4 ++-- .../tasks/optimization.py | 9 ++++----- src/pbn_export_queue/tests/test_admin.py | 12 ++++++++---- 8 files changed, 42 insertions(+), 20 deletions(-) create mode 100644 src/bpp/newsfragments/+naive-datetime-db-writes.bugfix.rst diff --git a/src/bpp/admin/templates.py b/src/bpp/admin/templates.py index 8cbb9273b..9e0fea879 100644 --- a/src/bpp/admin/templates.py +++ b/src/bpp/admin/templates.py @@ -1,6 +1,6 @@ import sys import traceback -from datetime import datetime, timedelta +from datetime import timedelta from dbtemplates.admin import TemplateAdmin, TemplateAdminForm from dbtemplates.models import Template @@ -13,6 +13,7 @@ from django.template.loaders.cached import Loader as CachedLoader from django.template.response import TemplateResponse from django.urls import re_path as url +from django.utils import timezone from bpp.util import rebuild_instances_of_models @@ -78,7 +79,7 @@ def template_updated(self, request, obj): return ILE_DNI = 7 - dni_temu = datetime.now() - timedelta(days=ILE_DNI) + dni_temu = timezone.now() - timedelta(days=ILE_DNI) messages.info( request, diff --git a/src/bpp/newsfragments/+naive-datetime-db-writes.bugfix.rst b/src/bpp/newsfragments/+naive-datetime-db-writes.bugfix.rst new file mode 100644 index 000000000..98dddeaff --- /dev/null +++ b/src/bpp/newsfragments/+naive-datetime-db-writes.bugfix.rst @@ -0,0 +1,18 @@ +Naprawiono zapis naive datetime do pól ``DateTimeField`` w kilku +miejscach kodu produkcyjnego, które używały ``datetime.now()`` +zamiast ``django.utils.timezone.now()``. Przy ``USE_TZ=True`` Django +wywoływało ``RuntimeWarning: received a naive datetime while time +zone support is active`` i interpretowało wartość w lokalnej strefie +czasowej — co przy zmianach DST mogło prowadzić do niespójności +dat w bazie. + +Zasięg zmian: + +- ``OptimizationRun.finished_at`` — zapisywane w + ``ewaluacja_optymalizacja.tasks.optimization`` oraz w komendach + ``solve_uczelnia`` i ``solve_evaluation``. +- ``remove_old_objects`` (``bpp.util``) — filtr wieku plików + używany m.in. przez ``remove_old_oswiadczenia_export_files`` + i ``remove_old_integrator_files``. +- ``TemplateAdmin.template_updated`` — filtr rekordów do + przebudowy cache opisu bibliograficznego. diff --git a/src/bpp/tests/test_multiseek/test_multiseek_misc.py b/src/bpp/tests/test_multiseek/test_multiseek_misc.py index db08ce845..502488d4c 100644 --- a/src/bpp/tests/test_multiseek/test_multiseek_misc.py +++ b/src/bpp/tests/test_multiseek/test_multiseek_misc.py @@ -12,8 +12,6 @@ - StatusKorektyQueryObject - wyszukiwanie po statusie korekty """ -from datetime import datetime - import pytest from model_bakery import baker from multiseek import logic @@ -31,12 +29,14 @@ ZewnetrznaBazaDanychQueryObject, ) +from django.utils import timezone + pytestmark = pytest.mark.serial @pytest.mark.django_db def test_OstatnioZmieniony(): - res = OstatnioZmieniony().real_query(datetime.now(), logic.EQUAL) + res = OstatnioZmieniony().real_query(timezone.now(), logic.EQUAL) assert Rekord.objects.filter(res).count() == 0 diff --git a/src/bpp/util.py b/src/bpp/util.py index 765af2432..434e7d4e1 100644 --- a/src/bpp/util.py +++ b/src/bpp/util.py @@ -3,7 +3,7 @@ import operator import os import re -from datetime import datetime, timedelta +from datetime import timedelta from functools import reduce from math import ceil, floor from pathlib import Path @@ -242,7 +242,7 @@ def zrob_cache(t): def remove_old_objects(klass, file_field="file", field_name="created_on", days=7): - since = datetime.now() - timedelta(days=days) + since = timezone.now() - timedelta(days=days) kwargs = {} kwargs[f"{field_name}__lt"] = since diff --git a/src/ewaluacja_optymalizacja/management/commands/solve_evaluation.py b/src/ewaluacja_optymalizacja/management/commands/solve_evaluation.py index 0b7f3e823..4a18103ec 100644 --- a/src/ewaluacja_optymalizacja/management/commands/solve_evaluation.py +++ b/src/ewaluacja_optymalizacja/management/commands/solve_evaluation.py @@ -1,9 +1,9 @@ import json -from datetime import datetime from decimal import Decimal from django.core.management import BaseCommand from django.db import transaction +from django.utils import timezone from bpp.models import ( Autor, @@ -293,7 +293,7 @@ def _save_optimization_to_database(self, results, dyscyplina): low_mono_count=results.low_mono_count, low_mono_percentage=Decimal(str(results.low_mono_percentage)), validation_passed=results.validation_passed, - finished_at=datetime.now(), + finished_at=timezone.now(), ) # Save author results diff --git a/src/ewaluacja_optymalizacja/management/commands/solve_uczelnia.py b/src/ewaluacja_optymalizacja/management/commands/solve_uczelnia.py index 319084337..9e32aad09 100644 --- a/src/ewaluacja_optymalizacja/management/commands/solve_uczelnia.py +++ b/src/ewaluacja_optymalizacja/management/commands/solve_uczelnia.py @@ -2,11 +2,11 @@ Management command to solve optimization for all disciplines in a university. """ -from datetime import datetime from decimal import Decimal from django.core.management import BaseCommand from django.db import transaction +from django.utils import timezone from bpp.models import Dyscyplina_Naukowa, Uczelnia from ewaluacja_liczba_n.models import IloscUdzialowDlaAutoraZaCalosc @@ -118,7 +118,7 @@ def _save_results(self, results, dyscyplina_nazwa, uczelnia_obj): low_mono_count=results.low_mono_count, low_mono_percentage=Decimal(str(results.low_mono_percentage)), validation_passed=results.validation_passed, - finished_at=datetime.now(), + finished_at=timezone.now(), ) # Save author results diff --git a/src/ewaluacja_optymalizacja/tasks/optimization.py b/src/ewaluacja_optymalizacja/tasks/optimization.py index be5278172..487a7a1cb 100644 --- a/src/ewaluacja_optymalizacja/tasks/optimization.py +++ b/src/ewaluacja_optymalizacja/tasks/optimization.py @@ -9,6 +9,7 @@ from celery import chord, group, shared_task from celery.exceptions import SoftTimeLimitExceeded from celery_singleton import Singleton +from django.utils import timezone from ewaluacja_liczba_n.models import LiczbaNDlaUczelni from ewaluacja_optymalizacja.core import solve_discipline @@ -114,7 +115,7 @@ def solve_single_discipline_task( if optimization_results.best_bound is not None else None ), - finished_at=datetime.now(), + finished_at=timezone.now(), ) # Zapisz wyniki autorów i publikacji @@ -185,7 +186,7 @@ def solve_single_discipline_task( status="failed", notes="Przekroczono limit czasu obliczeń (10 minut). " "Spróbuj ponownie lub sprawdź logi systemu.", - finished_at=datetime.now(), + finished_at=timezone.now(), ) except Exception as db_error: logger.error(f"Failed to save timeout error to database: {db_error}") @@ -212,7 +213,7 @@ def solve_single_discipline_task( uczelnia=uczelnia, status="failed", notes=f"Error: {type(e).__name__}: {str(e)}\n\nTraceback:\n{tb}", - finished_at=datetime.now(), + finished_at=timezone.now(), ) except Exception as db_error: logger.error(f"Failed to save error to database: {db_error}") @@ -227,8 +228,6 @@ def finalize_browser_recalc(results, uczelnia_id): Aktualizuje StatusPrzegladarkaRecalc oraz StatusOptymalizacjiBulk, aby użytkownik widział poprawny status nawet po odświeżeniu strony. """ - from django.utils import timezone - from ewaluacja_optymalizacja.models import ( StatusOptymalizacjiBulk, StatusPrzegladarkaRecalc, diff --git a/src/pbn_export_queue/tests/test_admin.py b/src/pbn_export_queue/tests/test_admin.py index 170562e32..9258e26e7 100644 --- a/src/pbn_export_queue/tests/test_admin.py +++ b/src/pbn_export_queue/tests/test_admin.py @@ -1,3 +1,4 @@ +from datetime import datetime from unittest.mock import patch import pytest @@ -11,6 +12,9 @@ from django.contrib.admin.sites import AdminSite from django.contrib.auth import get_user_model +from django.utils import timezone + +WYSYLKA_ZAKONCZONA_AT = timezone.make_aware(datetime(2023, 1, 1)) @pytest.mark.django_db @@ -36,7 +40,7 @@ def test_pbn_export_queue_admin_resend_single_item(wydawnictwo_ciagle, admin_use PBN_Export_Queue, rekord_do_wysylki=wydawnictwo_ciagle, zamowil=admin_user, - wysylke_zakonczono="2023-01-01 00:00:00", + wysylke_zakonczono=WYSYLKA_ZAKONCZONA_AT, zakonczono_pomyslnie=True, retry_after_user_authorised=True, ) @@ -69,7 +73,7 @@ def test_pbn_export_queue_admin_resend_action(wydawnictwo_ciagle, admin_user, rf PBN_Export_Queue, rekord_do_wysylki=wydawnictwo_ciagle, zamowil=admin_user, - wysylke_zakonczono="2023-01-01 00:00:00", + wysylke_zakonczono=WYSYLKA_ZAKONCZONA_AT, zakonczono_pomyslnie=True, ) @@ -77,7 +81,7 @@ def test_pbn_export_queue_admin_resend_action(wydawnictwo_ciagle, admin_user, rf PBN_Export_Queue, rekord_do_wysylki=wydawnictwo_ciagle, zamowil=admin_user, - wysylke_zakonczono="2023-01-01 00:00:00", + wysylke_zakonczono=WYSYLKA_ZAKONCZONA_AT, zakonczono_pomyslnie=False, ) @@ -126,7 +130,7 @@ def test_pbn_export_queue_admin_response_change_resend( PBN_Export_Queue, rekord_do_wysylki=wydawnictwo_ciagle, zamowil=admin_user, - wysylke_zakonczono="2023-01-01 00:00:00", + wysylke_zakonczono=WYSYLKA_ZAKONCZONA_AT, zakonczono_pomyslnie=True, ) From ecce1291fe6a181f65c1123fd9ea6b2bef3e6a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 17:33:49 +0200 Subject: [PATCH 02/15] chore(gitignore): exclude local TODO-*.txt / TODO-*.md audit notes Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 9f140d96a..81df68d02 100644 --- a/.gitignore +++ b/.gitignore @@ -169,3 +169,7 @@ opencode.jsonc dump.rdb .worktrees/ .claude/ + +# Local TODO / audit notes (not committed) +TODO-*.txt +TODO-*.md From 29cbef87565b2f4bdbc88e198544c6c720f61442 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 17:36:24 +0200 Subject: [PATCH 03/15] fix(tests): remove pytest.mark.django_db from fixtures MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pytest ostrzegał PytestRemovedIn9Warning "Marks applied to fixtures have no effect" (49 wystąpień w logach). Markery na fixturach i tak nie mają efektu — dostęp do bazy dziedziczy się z testu wywołującego, który ma własny @pytest.mark.django_db. W pytest 9 bedzie to blad. Dotyczy 7 fixtur w: - src/conftest.py - src/import_polon/tests/conftest.py - src/bpp/tests/test_models/test_sloty/conftest.py Co-Authored-By: Claude Opus 4.7 (1M context) --- src/bpp/newsfragments/+pytest-mark-on-fixtures.bugfix.rst | 6 ++++++ src/bpp/tests/test_models/test_sloty/conftest.py | 5 ----- src/conftest.py | 1 - src/import_polon/tests/conftest.py | 1 - 4 files changed, 6 insertions(+), 7 deletions(-) create mode 100644 src/bpp/newsfragments/+pytest-mark-on-fixtures.bugfix.rst diff --git a/src/bpp/newsfragments/+pytest-mark-on-fixtures.bugfix.rst b/src/bpp/newsfragments/+pytest-mark-on-fixtures.bugfix.rst new file mode 100644 index 000000000..f217f6fda --- /dev/null +++ b/src/bpp/newsfragments/+pytest-mark-on-fixtures.bugfix.rst @@ -0,0 +1,6 @@ +Usunięto redundantne dekoratory ``@pytest.mark.django_db`` nałożone +na fixtury w plikach ``conftest.py``. Pytest 8 ostrzegał +``PytestRemovedIn9Warning: Marks applied to fixtures have no +effect``, a sam marker i tak nie miał efektu — dostęp do bazy +danych w fixturach jest dziedziczony z testu wywołującego. W pytest 9 +stosowanie markerów na fixturach będzie błędem. diff --git a/src/bpp/tests/test_models/test_sloty/conftest.py b/src/bpp/tests/test_models/test_sloty/conftest.py index 48b5f6459..5666f6085 100644 --- a/src/bpp/tests/test_models/test_sloty/conftest.py +++ b/src/bpp/tests/test_models/test_sloty/conftest.py @@ -12,7 +12,6 @@ @pytest.fixture -@pytest.mark.django_db def zwarte_z_dyscyplinami( wydawnictwo_zwarte, autor_jan_nowak, @@ -56,7 +55,6 @@ def zwarte_z_dyscyplinami( @pytest.fixture -@pytest.mark.django_db def zwarte_z_dyscyplinami_hst( wydawnictwo_zwarte, autor_jan_nowak, @@ -93,7 +91,6 @@ def zwarte_z_dyscyplinami_hst( @pytest.fixture -@pytest.mark.django_db def zwarte_z_dyscyplinami_hst_oraz_nie_hst( wydawnictwo_zwarte, autor_jan_nowak, @@ -137,7 +134,6 @@ def zwarte_z_dyscyplinami_hst_oraz_nie_hst( @pytest.fixture -@pytest.mark.django_db def rodzaj_autora_n(): return Rodzaj_Autora.objects.get_or_create( skrot="N", defaults={"licz_sloty": True, "sort": 1} @@ -145,7 +141,6 @@ def rodzaj_autora_n(): @pytest.fixture -@pytest.mark.django_db def ciagle_z_dyscyplinami( wydawnictwo_ciagle, autor_jan_nowak, diff --git a/src/conftest.py b/src/conftest.py index b8f7e0087..f8a4a3f1f 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -322,7 +322,6 @@ def pbn_dyscyplina1(db, pbn_discipline_group): @pytest.fixture -@pytest.mark.django_db def zwarte_z_dyscyplinami( wydawnictwo_zwarte, autor_jan_nowak, diff --git a/src/import_polon/tests/conftest.py b/src/import_polon/tests/conftest.py index 3cd79edb4..71d3e34af 100644 --- a/src/import_polon/tests/conftest.py +++ b/src/import_polon/tests/conftest.py @@ -21,7 +21,6 @@ def fn_test_import_absencji(): @pytest.fixture -@pytest.mark.django_db def zwarte_z_dyscyplinami( wydawnictwo_zwarte, autor_jan_nowak, From 1dabd830a59970172e6f19134ec0c17d4419f0dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 17:40:38 +0200 Subject: [PATCH 04/15] test(create_test_db): convert placeholder to pytest-style MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Zgodnie z konwencja projektu (CLAUDE.md: "NEVER create unittest.TestCase tests") — zamiana TestCase na zwykla funkcje pytest. Usuniecie return True eliminuje DeprecationWarning "It is deprecated to return a value that is not None from a test case". Co-Authored-By: Claude Opus 4.7 (1M context) --- src/create_test_db/tests.py | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/create_test_db/tests.py b/src/create_test_db/tests.py index 9b36a5d09..144cdf619 100644 --- a/src/create_test_db/tests.py +++ b/src/create_test_db/tests.py @@ -1,10 +1,2 @@ -from django.test import TestCase - -# Create your tests here. - - -class TestCreateTestDb(TestCase): - def test_create_test_db(self): - """Pusty testcase - jego uruchomienie zawsze kończy się powodzeniem""" - - return True +def test_create_test_db(): + """Placeholder — jego uruchomienie zawsze kończy się powodzeniem.""" From b690859601e79aba77ac5e3fd902a79704fa47c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 17:41:45 +0200 Subject: [PATCH 05/15] fix(xlsx): use Font() constructor instead of deprecated cell.font.copy() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit openpyxl deprecated StyleProxy.copy() method — used Font() konstruktor z przepisanymi atrybutami zachowujacymi istniejace wartosci name/size i doklada bold=True. Eliminuje DeprecationWarning "Call to deprecated function copy" w pbn_wysylka_oswiadczen xlsx export. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/pbn_wysylka_oswiadczen/views.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pbn_wysylka_oswiadczen/views.py b/src/pbn_wysylka_oswiadczen/views.py index 4d796ed25..fa7b8e1e6 100644 --- a/src/pbn_wysylka_oswiadczen/views.py +++ b/src/pbn_wysylka_oswiadczen/views.py @@ -8,6 +8,7 @@ from django.views import View from django.views.generic import TemplateView from openpyxl import Workbook +from openpyxl.styles import Font from queryset_sequence import QuerySetSequence from bpp.const import GR_WPROWADZANIE_DANYCH @@ -370,7 +371,7 @@ def get(self, request): # Style header row for cell in ws[1]: - cell.font = cell.font.copy(bold=True) + cell.font = Font(bold=True, name=cell.font.name, size=cell.font.size) # Data rows for publication in combined_qs: From 7c6de7d9fe92c45ae2e723441eb100d11e2891ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 17:44:41 +0200 Subject: [PATCH 06/15] test: use wydawca_opis= directly instead of deprecated wydawnictwo= setter Wydawnictwo_Zwarte.wydawnictwo to property z deprekacja, ktore emituje DeprecationWarning "W przyszlosci uzyj wydawca_opis" przy zapisie. Zamiana w 2 testach (test_cache.py, test_history.py) na bezposrednie ustawianie wydawca_opis. Przy okazji ruff isort uporzadkowal importy w test_history.py. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/bpp/tests/test_cache/test_cache.py | 2 +- src/przemapuj_prace_autora/test_history.py | 9 ++++----- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/bpp/tests/test_cache/test_cache.py b/src/bpp/tests/test_cache/test_cache.py index 6df0f2d0d..e3acf58b8 100644 --- a/src/bpp/tests/test_cache/test_cache.py +++ b/src/bpp/tests/test_cache/test_cache.py @@ -536,7 +536,7 @@ def cache_setup(db): zwarte_dane = dict( miejsce_i_rok="Lublin 2012", - wydawnictwo="Pholium", + wydawca_opis="Pholium", redakcja="Redkacja", isbn="isbn", e_isbn="e_isbn", diff --git a/src/przemapuj_prace_autora/test_history.py b/src/przemapuj_prace_autora/test_history.py index efd962fd5..673e5a332 100644 --- a/src/przemapuj_prace_autora/test_history.py +++ b/src/przemapuj_prace_autora/test_history.py @@ -1,12 +1,9 @@ import pytest +from django.contrib.auth import get_user_model from django.test import Client from django.urls import reverse from model_bakery import baker -from .models import PrzemapoaniePracAutora - -from django.contrib.auth import get_user_model - from bpp.models import ( Autor, Jednostka, @@ -18,6 +15,8 @@ Zrodlo, ) +from .models import PrzemapoaniePracAutora + User = get_user_model() @@ -110,7 +109,7 @@ def test_przemapowanie_stores_work_history( tytul_oryginalny="Book Title", rok=2023, isbn="978-1234567890", - wydawnictwo="Test Publisher", + wydawca_opis="Test Publisher", ) baker.make( Wydawnictwo_Zwarte_Autor, From 064da89eae34124a336e75645a140c50fdc0cd58 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 18:02:33 +0200 Subject: [PATCH 07/15] test(long_running): move TestReport to test_bpp app MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Definiowanie `class TestReport(Report)` w ciele fixture'a powodowało ponowną rejestrację modelu `long_running.testreport` przy każdym uruchomieniu testu korzystającego z fixture'a `report`, co Django sygnalizowało `RuntimeWarning: Model 'long_running.testreport' was already registered. Reloading models is not advised...`. TestReport ma teraz stałą definicję w `test_bpp.models` wraz z migracją `0003_testreport`, a fixture sprowadza się do prostego `baker.make(TestReport)`. Nieużywany nigdzie override `send_notification` (dziedziczony NullNotificationMixin i tak jest no-opem) został usunięty. --- .../+testreport-double-register.bugfix.rst | 6 +++ src/long_running/tests/conftest.py | 19 +------ src/test_bpp/migrations/0003_testreport.py | 52 +++++++++++++++++++ src/test_bpp/models.py | 7 ++- 4 files changed, 66 insertions(+), 18 deletions(-) create mode 100644 src/bpp/newsfragments/+testreport-double-register.bugfix.rst create mode 100644 src/test_bpp/migrations/0003_testreport.py diff --git a/src/bpp/newsfragments/+testreport-double-register.bugfix.rst b/src/bpp/newsfragments/+testreport-double-register.bugfix.rst new file mode 100644 index 000000000..ab962d8e0 --- /dev/null +++ b/src/bpp/newsfragments/+testreport-double-register.bugfix.rst @@ -0,0 +1,6 @@ +Usunięto ``RuntimeWarning: Model 'long_running.testreport' was +already registered`` w testach ``long_running``. Testowy model +``TestReport`` został przeniesiony z inline'owej definicji we +fixturze do ``test_bpp.models`` wraz z migracją, dzięki czemu +model jest rejestrowany w ``apps`` tylko raz, a nie przy każdym +wywołaniu fixture'a. diff --git a/src/long_running/tests/conftest.py b/src/long_running/tests/conftest.py index c8635cd54..1a6818f37 100644 --- a/src/long_running/tests/conftest.py +++ b/src/long_running/tests/conftest.py @@ -1,9 +1,7 @@ import pytest -from django.db import connection from model_bakery import baker -from long_running.models import Report -from test_bpp.models import TestOperation +from test_bpp.models import TestOperation, TestReport @pytest.fixture @@ -13,17 +11,4 @@ def operation(admin_user): @pytest.fixture def report(db): - class TestReport(Report): - sent_notifications = [] - - def send_notification(self, *args, **kw): - self.sent_notifications.append((args, kw)) - - # Create the schema for our test model - with connection.schema_editor() as schema_editor: - schema_editor.create_model(TestReport) - - yield baker.make(TestReport) - - # with connection.schema_editor() as schema_editor: - # schema_editor.delete_model(TestReport) + return baker.make(TestReport) diff --git a/src/test_bpp/migrations/0003_testreport.py b/src/test_bpp/migrations/0003_testreport.py new file mode 100644 index 000000000..290ea2936 --- /dev/null +++ b/src/test_bpp/migrations/0003_testreport.py @@ -0,0 +1,52 @@ +# Generated by Django 4.2.30 on 2026-04-19 16:00 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import long_running.notification_mixins +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ("test_bpp", "0002_testobjectthatdoesnotexist"), + ] + + operations = [ + migrations.CreateModel( + name="TestReport", + fields=[ + ( + "id", + models.UUIDField( + default=uuid.uuid4, + editable=False, + primary_key=True, + serialize=False, + ), + ), + ("created_on", models.DateTimeField(auto_now_add=True)), + ("last_updated_on", models.DateTimeField(auto_now=True)), + ("started_on", models.DateTimeField(blank=True, null=True)), + ("finished_on", models.DateTimeField(blank=True, null=True)), + ("finished_successfully", models.BooleanField(default=False)), + ("traceback", models.TextField(blank=True, null=True)), + ( + "owner", + models.ForeignKey( + on_delete=django.db.models.deletion.CASCADE, + to=settings.AUTH_USER_MODEL, + ), + ), + ], + options={ + "abstract": False, + }, + bases=( + long_running.notification_mixins.NullNotificationMixin, + models.Model, + ), + ), + ] diff --git a/src/test_bpp/models.py b/src/test_bpp/models.py index f3ea17246..9a966a0c6 100644 --- a/src/test_bpp/models.py +++ b/src/test_bpp/models.py @@ -1,7 +1,7 @@ # Create your models here. from django.db import models -from long_running.models import Operation +from long_running.models import Operation, Report class TestOperation(Operation): @@ -10,6 +10,11 @@ class TestOperation(Operation): # long_running.tests.test_models +class TestReport(Report): + # long_running.tests.test_models + pass + + class TestObjectThatDoesNotExistManager(models.Manager): # long_running.tests.test_tasks def get(self, *args, **kw): From c93daa819f47788c98b9c8597a7829fc32b9654f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 18:28:05 +0200 Subject: [PATCH 08/15] fix(tables): align Meta.model with actual queryset row type MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit django-tables2 emits `UserWarning: Table data is of type but is specified in Table.Meta.model` when the declared Meta.model does not match the row type of the queryset backing the table. `Meta.model` is only used for field introspection, so the mismatch was cosmetic — fix it by pointing it at the actual row type: - RankingAutorowTable: Autor -> Sumy (queryset is Sumy.objects...) - RaportSlotowUczelniaTable: Cache_Punktacja_Autora_Query -> RaportSlotowUczelniaWiersz (list view iterates over raportslotowuczelniawiersz_set) Drop now-unused `Autor` / `Cache_Punktacja_Autora_Query` imports. --- .../+tables-meta-model-mismatch.bugfix.rst | 16 ++++++++++++++++ src/ranking_autorow/views.py | 3 +-- src/raport_slotow/tables.py | 7 ++----- 3 files changed, 19 insertions(+), 7 deletions(-) create mode 100644 src/bpp/newsfragments/+tables-meta-model-mismatch.bugfix.rst diff --git a/src/bpp/newsfragments/+tables-meta-model-mismatch.bugfix.rst b/src/bpp/newsfragments/+tables-meta-model-mismatch.bugfix.rst new file mode 100644 index 000000000..8e3624c46 --- /dev/null +++ b/src/bpp/newsfragments/+tables-meta-model-mismatch.bugfix.rst @@ -0,0 +1,16 @@ +Wyrównano ``class Meta: model = ...`` w tabelach ``django-tables2`` +do faktycznego typu wierszy w QuerySecie. Dotychczas wyświetlane były +ostrzeżenia ``UserWarning: Table data is of type but is +specified in Table.Meta.model``: + +- ``RankingAutorowTable`` — ``model = Autor`` → ``model = Sumy`` + (dane pochodzą z ``Nowe_Sumy_View`` / ``Sumy``, nie bezpośrednio + z ``Autor``). +- ``RaportSlotowUczelniaTable`` — ``model = + Cache_Punktacja_Autora_Query`` → ``model = + RaportSlotowUczelniaWiersz`` (widok listy iteruje po rekordach + ``RaportSlotowUczelnia.raportslotowuczelniawiersz_set``). + +``Meta.model`` w ``django-tables2`` służy tylko do introspekcji pól; +poza zniknięciem samego ostrzeżenia zachowanie tabel nie uległo +zmianie. diff --git a/src/ranking_autorow/views.py b/src/ranking_autorow/views.py index c09e66060..109439cf5 100644 --- a/src/ranking_autorow/views.py +++ b/src/ranking_autorow/views.py @@ -13,7 +13,6 @@ from django_tables2.views import SingleTableView from bpp.models import ( - Autor, Charakter_Formalny, Jednostka, OpcjaWyswietlaniaField, @@ -102,7 +101,7 @@ def form_valid(self, form): class RankingAutorowTable(Table): class Meta: attrs = {"class": "bpp-table"} - model = Autor + model = Sumy order_by = ("-impact_factor_sum", "autor__nazwisko") fields = ( "lp", diff --git a/src/raport_slotow/tables.py b/src/raport_slotow/tables.py index 191b669e8..26e7f86bb 100644 --- a/src/raport_slotow/tables.py +++ b/src/raport_slotow/tables.py @@ -6,10 +6,7 @@ from django_tables2 import Column from bpp.models import CHARAKTER_SLOTY -from bpp.models.cache import ( - Cache_Punktacja_Autora_Query, - Cache_Punktacja_Autora_Query_View, -) +from bpp.models.cache import Cache_Punktacja_Autora_Query_View from raport_slotow import const from raport_slotow.columns import DecimalColumn, SummingColumn from raport_slotow.models import ( @@ -158,7 +155,7 @@ def value_autor(self, value): class RaportSlotowUczelniaTable(RaportSlotowUczelniaBezJednostekIWydzialowTable): class Meta: empty_text = "Brak danych" - model = Cache_Punktacja_Autora_Query + model = RaportSlotowUczelniaWiersz fields = ( "autor", "pbn_id", From c804f18a207539092336fb4e0253cceb5ba26d56 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 18:34:06 +0200 Subject: [PATCH 09/15] fix(pagination): add stable order_by to paginated querysets MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Django emits `UnorderedObjectListWarning` and may serve duplicate or missing rows across pages when a Paginator is handed an unordered queryset. Fix the callers surfaced by the current test run: Autocomplete (bpp.views.autocomplete): - Dyscyplina_NaukowaAutocomplete — order by (kod, nazwa) - Wydawnictwo_NadrzedneAutocomplete + admin variants — order by (tytul_oryginalny, pk) - PublicZrodloAutocomplete / ZrodloAutocomplete — order by (nazwa, pk); apply to both the base queryset and the QuerySetSequence combining PBN-priority branches Paginated list views: - pbn_wysylka_oswiadczen.PublicationListView — order the combined QuerySetSequence by (-rok, tytul_oryginalny, pk) - RaportSlotowUczelnia.get_details_set — order raportslotowuczelnia wiersz_set by (autor__nazwisko, autor__imiona, pk) Model Meta: - RozbieznosciView — restore `ordering = ["id"]` that the abstract base had but the concrete Meta was silently overriding. New migration 0021 is pure AlterModelOptions (managed=False, no DDL). Pre-existing ruff findings in raport_slotow/models/uczelnia.py (C901, unused loop variable) silenced without refactoring — scope is the pagination fix, not `create_report` internals. --- .../+pagination-ordered-querysets.bugfix.rst | 22 +++++++++++++++++++ src/bpp/views/autocomplete/publications.py | 8 +++---- src/bpp/views/autocomplete/simple.py | 8 +++---- src/pbn_wysylka_oswiadczen/views.py | 8 +++++-- src/raport_slotow/models/uczelnia.py | 19 +++++++++------- .../0021_alter_rozbieznosciview_options.py | 22 +++++++++++++++++++ src/rozbieznosci_dyscyplin/models.py | 1 + 7 files changed, 70 insertions(+), 18 deletions(-) create mode 100644 src/bpp/newsfragments/+pagination-ordered-querysets.bugfix.rst create mode 100644 src/rozbieznosci_dyscyplin/migrations/0021_alter_rozbieznosciview_options.py diff --git a/src/bpp/newsfragments/+pagination-ordered-querysets.bugfix.rst b/src/bpp/newsfragments/+pagination-ordered-querysets.bugfix.rst new file mode 100644 index 000000000..d4d769a39 --- /dev/null +++ b/src/bpp/newsfragments/+pagination-ordered-querysets.bugfix.rst @@ -0,0 +1,22 @@ +Dodano stabilne ``order_by`` do QuerySetów, które były stronicowane +bez jawnego sortowania. Django emitowało wtedy +``UnorderedObjectListWarning: Pagination may yield inconsistent +results with an unordered object_list``, a kolejne strony mogły +zwracać zduplikowane lub pominięte rekordy. + +Poprawione miejsca: + +- Autocomplete w ``bpp.views.autocomplete``: ``Dyscyplina_Naukowa`` + (``kod``), ``Wydawnictwo_Zwarte`` dla wydawnictwa nadrzędnego i + wariantów admina (``tytul_oryginalny``), ``Wydawnictwo_Ciagle`` + admin (``tytul_oryginalny``), ``Zrodlo`` (``nazwa`` — zarówno + bazowy queryset jak i ``QuerySetSequence`` z priorytetami PBN). +- ``pbn_wysylka_oswiadczen.views.PublicationListView`` — combined + ``QuerySetSequence`` sortowany ``-rok, tytul_oryginalny, pk``. +- ``RaportSlotowUczelnia.get_details_set()`` — sortowanie po + ``autor__nazwisko, autor__imiona, pk`` dla stabilnej paginacji + szczegółów raportu. +- ``RozbieznosciView`` — dodano ``Meta.ordering = ["id"]`` (bazowy + abstrakcyjny model już miał tę opcję, ale lokalne ``Meta`` ją + nadpisywało). Migracja ``0021`` to wyłącznie ``AlterModelOptions`` + (model jest ``managed = False``, brak DDL). diff --git a/src/bpp/views/autocomplete/publications.py b/src/bpp/views/autocomplete/publications.py index 8b3bb8774..80c43e0af 100644 --- a/src/bpp/views/autocomplete/publications.py +++ b/src/bpp/views/autocomplete/publications.py @@ -18,7 +18,7 @@ class Wydawnictwo_NadrzedneAutocomplete( def get_queryset(self): qs = Wydawnictwo_Zwarte.objects.filter( charakter_formalny__charakter_ogolny=CHARAKTER_OGOLNY_KSIAZKA - ) + ).order_by("tytul_oryginalny", "pk") if self.q: qs = qs.filter(tytul_oryginalny__icontains=self.q) @@ -31,7 +31,7 @@ class Wydawnictwo_CiagleAdminAutocomplete( """Admin autocomplete for continuous publications.""" def get_queryset(self): - qs = Wydawnictwo_Ciagle.objects.all() + qs = Wydawnictwo_Ciagle.objects.all().order_by("tytul_oryginalny", "pk") if self.q: qs = qs.filter(tytul_oryginalny__icontains=self.q) return qs @@ -43,7 +43,7 @@ class Wydawnictwo_ZwarteAdminAutocomplete( """Admin autocomplete for monographic publications.""" def get_queryset(self): - qs = Wydawnictwo_Zwarte.objects.all() + qs = Wydawnictwo_Zwarte.objects.all().order_by("tytul_oryginalny", "pk") if self.q: qs = qs.filter(tytul_oryginalny__icontains=self.q) return qs @@ -69,7 +69,7 @@ def get_queryset(self): pk__in=Wydawnictwo_Zwarte.objects.exclude(wydawnictwo_nadrzedne_id=None) .values_list("wydawnictwo_nadrzedne_id") .distinct() - ) + ).order_by("tytul_oryginalny", "pk") if self.q: qs = qs.filter(tytul_oryginalny__icontains=self.q) diff --git a/src/bpp/views/autocomplete/simple.py b/src/bpp/views/autocomplete/simple.py index d553254c0..199889b95 100644 --- a/src/bpp/views/autocomplete/simple.py +++ b/src/bpp/views/autocomplete/simple.py @@ -225,7 +225,7 @@ def _build_token_filter(self, token): return qobj def get_queryset(self): - qs = self._get_base_queryset() + qs = self._get_base_queryset().order_by("nazwa", "pk") if self.q: for token in [x.strip() for x in self.q.split(" ") if x.strip()]: qs = qs.filter(self._build_token_filter(token)) @@ -243,7 +243,7 @@ def _get_base_queryset(self): return Zrodlo.objects.all().select_related("pbn_uid") def get_queryset(self): - qs = self._get_base_queryset() + qs = self._get_base_queryset().order_by("nazwa", "pk") if self.q: for token in [x.strip() for x in self.q.split(" ") if x.strip()]: qs = qs.filter(self._build_token_filter(token)) @@ -260,7 +260,7 @@ def get_queryset(self): # Use QuerySetSequence to chain querysets with priority res = QuerySetSequence( qs_with_full_pbn, qs_with_pbn_no_mnisw, qs_without_pbn - ) + ).order_by("nazwa", "pk") res.model = Zrodlo # django-autocomplete-light needs this return res @@ -315,7 +315,7 @@ class Dyscyplina_NaukowaAutocomplete( """Autocomplete for scientific disciplines.""" def get_queryset(self): - qs = Dyscyplina_Naukowa.objects.filter(widoczna=True) + qs = Dyscyplina_Naukowa.objects.filter(widoczna=True).order_by("kod", "nazwa") if self.q: qs = qs.filter(Q(nazwa__icontains=self.q) | Q(kod__icontains=self.q)) return qs diff --git a/src/pbn_wysylka_oswiadczen/views.py b/src/pbn_wysylka_oswiadczen/views.py index fa7b8e1e6..5d4772a2c 100644 --- a/src/pbn_wysylka_oswiadczen/views.py +++ b/src/pbn_wysylka_oswiadczen/views.py @@ -117,8 +117,12 @@ def get_context_data(self, **kwargs): rok_od, rok_do, tytul or None, tylko_odpiete, with_annotations=True ) - # Combine querysets - combined_qs = QuerySetSequence(ciagle_qs, zwarte_qs) + # Combine querysets with stable ordering — QuerySetSequence passes + # the order_by down to each underlying queryset, which lets + # Paginator produce consistent pages. + combined_qs = QuerySetSequence(ciagle_qs, zwarte_qs).order_by( + "-rok", "tytul_oryginalny", "pk" + ) # Paginate paginator = Paginator(combined_qs, per_page) diff --git a/src/raport_slotow/models/uczelnia.py b/src/raport_slotow/models/uczelnia.py index 6236d410c..01a214c58 100644 --- a/src/raport_slotow/models/uczelnia.py +++ b/src/raport_slotow/models/uczelnia.py @@ -18,14 +18,13 @@ from django.core.validators import MaxValueValidator from django.db import models -from long_running.models import Report -from long_running.notification_mixins import ASGINotificationMixin -from raport_slotow.core import autorzy_zerowi - from bpp.core import zbieraj_sloty from bpp.fields import YearField from bpp.models import Autor, Cache_Punktacja_Autora_Query, Uczelnia from bpp.util import year_last_month +from long_running.models import Report +from long_running.notification_mixins import ASGINotificationMixin +from raport_slotow.core import autorzy_zerowi class RaportSlotowUczelnia(ASGINotificationMixin, Report): @@ -98,7 +97,7 @@ def clean(self): } ) - def create_report(self): + def create_report(self): # noqa: C901 (pre-existing complexity) # lista wszystkich autorow z punktacja z okresu od-do roku lst = "autor_id", "dyscyplina_id" if self.dziel_na_jednostki_i_wydzialy: @@ -183,7 +182,7 @@ def create_report(self): ).distinct() ) - for autor_id, rok, dyscyplina_id in zerowi.values_list(): + for autor_id, _rok, dyscyplina_id in zerowi.values_list(): if (autor_id, dyscyplina_id) in seen: continue @@ -214,8 +213,12 @@ def create_report(self): self.raportslotowuczelniawiersz_set.create(**kw) def get_details_set(self): - return self.raportslotowuczelniawiersz_set.all().select_related( - "autor", "autor__tytul", "jednostka", "jednostka__wydzial", "dyscyplina" + return ( + self.raportslotowuczelniawiersz_set.all() + .select_related( + "autor", "autor__tytul", "jednostka", "jednostka__wydzial", "dyscyplina" + ) + .order_by("autor__nazwisko", "autor__imiona", "pk") ) diff --git a/src/rozbieznosci_dyscyplin/migrations/0021_alter_rozbieznosciview_options.py b/src/rozbieznosci_dyscyplin/migrations/0021_alter_rozbieznosciview_options.py new file mode 100644 index 000000000..d65be75ac --- /dev/null +++ b/src/rozbieznosci_dyscyplin/migrations/0021_alter_rozbieznosciview_options.py @@ -0,0 +1,22 @@ +# Generated by Django 4.2.30 on 2026-04-19 16:32 + +from django.db import migrations + + +class Migration(migrations.Migration): + + dependencies = [ + ("rozbieznosci_dyscyplin", "0020_recreate"), + ] + + operations = [ + migrations.AlterModelOptions( + name="rozbieznosciview", + options={ + "managed": False, + "ordering": ["id"], + "verbose_name": "rozbieżność rekordu i dyscyplin", + "verbose_name_plural": "rozbieżności rekordów i dyscyplin", + }, + ), + ] diff --git a/src/rozbieznosci_dyscyplin/models.py b/src/rozbieznosci_dyscyplin/models.py index f33b3053a..902832fe0 100644 --- a/src/rozbieznosci_dyscyplin/models.py +++ b/src/rozbieznosci_dyscyplin/models.py @@ -41,6 +41,7 @@ class RozbieznosciView(RozbieznosciViewBase): # oraz autora) to ten model i funkcja get_wydawnictwo_autor_obj zawiedzie. class Meta: managed = False + ordering = ["id"] verbose_name = "rozbieżność rekordu i dyscyplin" verbose_name_plural = "rozbieżności rekordów i dyscyplin" From 136e2c8a36bcf182ab0753158e4d46b1aff69ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 19:27:55 +0200 Subject: [PATCH 10/15] fix(long_running): wait_for_object on celery retry instead of time.sleep MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Dotychczas funkcja blokowała workera 10× `time.sleep(1)` czekając, aż obiekt utworzony w innej transakcji pojawi się w bazie. Teraz w razie `DoesNotExist` woła `current_task.retry(countdown=1, max_retries=no_tries)` — celery ponownie uruchomi to samo zadanie za sekundę, a worker w tym czasie obsługuje inne zadania. Po wyczerpaniu prób celery podnosi oryginalny `DoesNotExist` (kontrakt `Task.retry(exc=...)`). Usunięto `DeprecationWarning` emitowany przy każdym wywołaniu. Kontrakt: funkcję wywołujemy wyłącznie z kontekstu zadania celery (`.delay(...)`, `.apply_async(...)`, `.apply(...)`). Wywołanie funkcji-zadania wprost nie ustawia `current_task` i omija retry. Testy `analyze_file` i `task_sprobuj_wyslac_do_pbn` przerobione z bezpośredniego wywołania na wywołanie przez celery. Tam gdzie test mockuje `task.apply_async` (do weryfikacji re-schedulowania w gałęziach RETRY_*) używam `task.apply(args=(...)).get()` zamiast `.delay(...).get()` — `.delay()` trafiłoby w mock apply_async i body zadania nigdy by się nie uruchomiło. Dodano trzy testy pokrywające kontrakt `wait_for_object` w `src/long_running/tests/test_util.py` (happy path, exhaust, succeed-on-retry). Używają `.apply(throw=False)` + fixture, która tymczasowo ustawia `app.conf.task_eager_propagates=False`, bo CELERY_EAGER_PROPAGATES_EXCEPTIONS=True w bpp sprawia, że `Retry` propaguje się poza `apply()` natychmiast i rekurencyjne `retval.sig.apply(retries=retries+1)` w `Task.apply` gubi `throw=False` (używa defaultu z app.conf). Produkcyjny worker tej fixture nie wymaga. --- .../+wait-for-object-celery-retry.bugfix.rst | 19 +++++ src/integrator2/tests/test_tasks.py | 5 +- src/long_running/tests/test_util.py | 80 +++++++++++++++++-- src/long_running/util.py | 52 +++++++----- src/pbn_export_queue/tests/test_tasks.py | 22 +++-- 5 files changed, 145 insertions(+), 33 deletions(-) create mode 100644 src/bpp/newsfragments/+wait-for-object-celery-retry.bugfix.rst diff --git a/src/bpp/newsfragments/+wait-for-object-celery-retry.bugfix.rst b/src/bpp/newsfragments/+wait-for-object-celery-retry.bugfix.rst new file mode 100644 index 000000000..8700d15c7 --- /dev/null +++ b/src/bpp/newsfragments/+wait-for-object-celery-retry.bugfix.rst @@ -0,0 +1,19 @@ +``long_running.util.wait_for_object`` nie blokuje już workera +``time.sleep``-em. W razie ``DoesNotExist`` woła +``current_task.retry(countdown=1, max_retries=no_tries)`` — celery +planuje ponowne uruchomienie tego samego zadania za sekundę, a worker +obsługuje w tym czasie inne zadania. Po wyczerpaniu prób celery +podnosi oryginalny ``DoesNotExist``. Zniknął też +``DeprecationWarning`` emitowany przy każdym wywołaniu. + +Kontrakt: funkcję wywołujemy wyłącznie z kontekstu zadania celery +(``task.delay(...)``, ``.apply_async(...)``, ``.apply(...)``). +Wywołanie funkcji-zadania wprost jako zwykłej funkcji +(``task_func(pk)``) nie ustawia ``current_task`` i omija mechanizm +retry. Testy, które wcześniej wołały ``analyze_file`` i +``task_sprobuj_wyslac_do_pbn`` bezpośrednio, zostały przerobione +na wywołanie przez celery (``.delay(...).get()`` albo +``.apply(args=..., ...).get()`` — ``.apply()`` potrzebne tam, gdzie +test mockuje ``task.apply_async`` do weryfikacji re-schedulowania +i wtedy ``.delay()`` trafiłoby w mock zamiast uruchomić body +zadania). diff --git a/src/integrator2/tests/test_tasks.py b/src/integrator2/tests/test_tasks.py index 8ed6e50a8..232378fee 100644 --- a/src/integrator2/tests/test_tasks.py +++ b/src/integrator2/tests/test_tasks.py @@ -11,7 +11,10 @@ @pytest.mark.django_db(transaction=True) def test_analyze_file(lmi): - res = analyze_file(pk=lmi.pk) + # Wait_for_object używany przez analyze_file wymaga kontekstu + # celery (current_task.retry), dlatego zadanie wołamy przez + # .delay() (eager mode wykonuje je synchronicznie). + res = analyze_file.delay(pk=lmi.pk).get() assert res is None diff --git a/src/long_running/tests/test_util.py b/src/long_running/tests/test_util.py index bb3fc3364..5e25ca965 100644 --- a/src/long_running/tests/test_util.py +++ b/src/long_running/tests/test_util.py @@ -1,14 +1,82 @@ import pytest +from django_bpp.celery_tasks import app from long_running.util import wait_for_object from bpp.models import Wydawnictwo_Ciagle +# Zadanie do testów — zarejestrowane na poziomie modułu (celery wymaga +# rejestracji przed .apply()/.delay()). +@app.task(bind=True, name="long_running.tests.test_util._probe_wait") +def _probe_wait(self, pk, no_tries): + wait_for_object(Wydawnictwo_Ciagle, pk, no_tries=no_tries) + + +@pytest.fixture +def celery_eager_retry_loops(): + """Pozwól celery zapętlić retry w eager mode. + + BPP ma globalnie ``CELERY_EAGER_PROPAGATES_EXCEPTIONS=True`` + (``app.conf.task_eager_propagates``). W eager mode tracer celery + przy ``propagate=True`` w ``on_error`` re-raise'uje wyjątek, także + ``Retry``. Efekt: ``apply(throw=False)`` ustawia ``propagate=False`` + TYLKO na pierwszej iteracji, ale rekurencyjne + ``retval.sig.apply(retries=retries + 1)`` w ``Task.apply`` nie + przenosi ``throw`` — używa domyślnego + ``task_eager_propagates=True``, propagacja wraca, pętla się rwie + na drugiej próbie. + + Fixture tymczasowo wyłącza ``task_eager_propagates`` na poziomie + konfiguracji celery app, żeby wszystkie rekurencyjne ``.apply()`` + również miały ``propagate=False``. Produkcyjny worker tej + fixture nie wymaga — tam ``Retry`` po prostu wraca do brokera + jako re-enqueue. + """ + prev = app.conf.task_eager_propagates + app.conf.task_eager_propagates = False + try: + yield + finally: + app.conf.task_eager_propagates = prev + + @pytest.mark.django_db -def test_wait_for_object(wydawnictwo_ciagle): - with pytest.warns(DeprecationWarning): - assert ( - wait_for_object(Wydawnictwo_Ciagle, wydawnictwo_ciagle.pk) - == wydawnictwo_ciagle - ) +def test_wait_for_object_happy_path(wydawnictwo_ciagle): + # Obiekt istnieje — .get() zwraca od razu, ścieżka retry nie jest + # dotykana. Zadanie kończy się SUCCESS. + result = _probe_wait.apply(args=(wydawnictwo_ciagle.pk, 10), throw=False) + assert result.successful(), result.result + + +@pytest.mark.django_db +def test_wait_for_object_retries_and_exhausts(celery_eager_retry_loops): + # Obiektu nigdy nie ma — celery wykonuje `no_tries` retry-ów, + # po czym (kontrakt Task.retry z jawnym exc) podnosi oryginalny + # DoesNotExist, a nie MaxRetriesExceededError. + result = _probe_wait.apply(args=(999999999, 3), throw=False) + with pytest.raises(Wydawnictwo_Ciagle.DoesNotExist): + result.get() + + +@pytest.mark.django_db +def test_wait_for_object_retries_then_succeeds( + wydawnictwo_ciagle, mocker, celery_eager_retry_loops +): + # Symulacja wyścigu commit vs. worker: pierwsze dwa .get() udają, + # że obiektu jeszcze nie ma, trzecie zwraca go. Dzięki celery retry + # zadanie kończy się sukcesem. + real_get = Wydawnictwo_Ciagle.objects.get + calls = {"n": 0} + + def fake_get(*args, **kw): + calls["n"] += 1 + if calls["n"] < 3: + raise Wydawnictwo_Ciagle.DoesNotExist + return real_get(*args, **kw) + + mocker.patch.object(Wydawnictwo_Ciagle.objects, "get", side_effect=fake_get) + + result = _probe_wait.apply(args=(wydawnictwo_ciagle.pk, 10), throw=False) + assert result.successful(), result.result + assert calls["n"] == 3 diff --git a/src/long_running/util.py b/src/long_running/util.py index 52d15bb63..9ea3d1482 100644 --- a/src/long_running/util.py +++ b/src/long_running/util.py @@ -1,28 +1,38 @@ -import time -import warnings +from celery import current_task def wait_for_object(klass, pk, no_tries=10): - warnings.warn( - "Ta funkcja niepotrzebnie 'przetrzymuje' workera przez 10 sekund. " - "Rozsadne byloby jej nie uzywac i przepisac kod na konstrukcje zblizona do" - "long_running.tasks.perform_generic_long_running_task -- czyli funkcje, ktora" - "probuje uruchomic sie za 10 sekund za pomoca mechanizmow celery, nie zas " - "blokujaca worker za pomoca time.sleep ... ", - category=DeprecationWarning, - ) + """Pobierz obiekt z bazy albo poproś celery o ponowne uruchomienie + bieżącego zadania za 1 sekundę. - obj = None + Przeznaczone do użycia w zadaniach celery, które startują zaraz po + utworzeniu obiektu w innej transakcji + (``transaction.on_commit(lambda: task.delay(pk))``): kiedy worker + wystartuje szybciej niż commit się upropaguje, obiekt jeszcze nie + istnieje. - while no_tries > 0: - try: - obj = klass.objects.get(pk=pk) - break - except klass.DoesNotExist: - time.sleep(1) - no_tries = no_tries - 1 + Jeżeli obiekt nie został odnaleziony, funkcja wywołuje + ``current_task.retry(countdown=1, max_retries=no_tries)``. Celery + w produkcji zwróci zadanie do kolejki; po ``no_tries`` nieudanych + próbach podnosi ``MaxRetriesExceededError`` / oryginalny + ``DoesNotExist``. Worker nie jest blokowany ``time.sleep``-em, a + górne ograniczenie liczby prób jest egzekwowane przez framework. - if obj is None: - raise klass.DoesNotExist("Cannot fetch klass %r with pk %r" % (klass, pk)) + UWAGA: w trybie ``CELERY_TASK_ALWAYS_EAGER=True`` z + ``CELERY_EAGER_PROPAGATES_EXCEPTIONS=True`` (ustawienia BPP dla + testów) celery **nie zapętla** retry wewnątrz ``.delay().get()``, + bo wyjątek ``Retry`` propaguje się natychmiast na zewnątrz + ``apply()``. Żeby uruchomić prawdziwą pętlę w testach, woła się + zadanie przez ``task.apply(args=..., throw=False)`` — wtedy + tracer nie propaguje wyjątku i ``apply`` rekurencyjnie wywołuje + sygnaturę zadania aż do wyczerpania ``max_retries``. - return obj + Funkcja musi być wywoływana z kontekstu zadania celery + (``task.delay(...)`` / ``apply_async(...)`` / ``apply(...)``). + Wywołanie funkcji-zadania wprost (``task_func(pk)``) nie ustawia + ``current_task`` i ominie mechanizm retry. + """ + try: + return klass.objects.get(pk=pk) + except klass.DoesNotExist as exc: + raise current_task.retry(exc=exc, countdown=1, max_retries=no_tries) from exc diff --git a/src/pbn_export_queue/tests/test_tasks.py b/src/pbn_export_queue/tests/test_tasks.py index c3c9c4531..4c4c64475 100644 --- a/src/pbn_export_queue/tests/test_tasks.py +++ b/src/pbn_export_queue/tests/test_tasks.py @@ -87,7 +87,14 @@ def test_task_sprobuj_wyslac_do_pbn_retry_later(mocker, send_status): wait_for_object.return_value = mock_peq - task_sprobuj_wyslac_do_pbn(5) + # Wait_for_object pod spodem używa current_task.retry → wymaga + # wywołania przez celery (tu eager). .delay() → .apply() → get(). + # Nie używamy .delay() bo niektóre testy mockują + # task_sprobuj_wyslac_do_pbn.apply_async (żeby zweryfikować + # re-schedulowanie w gałęzi RETRY_*). .delay() przeszłoby przez + # tamten mock i body zadania nigdy by się nie uruchomiło. + # .apply(args=...) idzie bezpośrednio do tracera eager mode. + task_sprobuj_wyslac_do_pbn.apply(args=(5,)).get() # Check that lock was acquired and released mock_cache_add.assert_called_once() @@ -121,7 +128,12 @@ def test_task_sprobuj_wyslac_do_pbn_finished(mocker, send_status): wait_for_object.return_value = mock_peq - task_sprobuj_wyslac_do_pbn(5) + # Nie używamy .delay() bo niektóre testy mockują + # task_sprobuj_wyslac_do_pbn.apply_async (żeby zweryfikować + # re-schedulowanie w gałęzi RETRY_*). .delay() przeszłoby przez + # tamten mock i body zadania nigdy by się nie uruchomiło. + # .apply(args=...) idzie bezpośrednio do tracera eager mode. + task_sprobuj_wyslac_do_pbn.apply(args=(5,)).get() # Check that lock was acquired and released mock_cache_add.assert_called_once() @@ -142,7 +154,7 @@ def test_task_sprobuj_wyslac_do_pbn_raises(mocker): wait_for_object.return_value = mock_peq with pytest.raises(NotImplementedError): - task_sprobuj_wyslac_do_pbn(5) + task_sprobuj_wyslac_do_pbn.apply(args=(5,)).get() # Lock should still be cleaned up even on error mock_cache_delete.assert_called_once() @@ -156,7 +168,7 @@ def test_task_sprobuj_wyslac_do_pbn_lock_already_acquired(mocker): wait_for_object = mocker.patch("pbn_export_queue.tasks.wait_for_object") - result = task_sprobuj_wyslac_do_pbn(5) + result = task_sprobuj_wyslac_do_pbn.apply(args=(5,)).get() # Should return immediately without processing assert result == "ALREADY_PROCESSING" @@ -177,7 +189,7 @@ def test_task_sprobuj_wyslac_do_pbn_already_completed(mocker): wait_for_object.return_value = mock_peq - result = task_sprobuj_wyslac_do_pbn(5) + result = task_sprobuj_wyslac_do_pbn.apply(args=(5,)).get() # Should return without calling send_to_pbn assert result == "ALREADY_COMPLETED" From cf42c592283fdc2edc217ed328535d3f1e5c12cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 19:35:08 +0200 Subject: [PATCH 11/15] fix(context_processor): use get_values_for_keys to avoid constance async warnings MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Od django-constance 4.x Config.__getattr__ wykrywa aktywną pętlę asyncio i zwraca AsyncValueProxy zamiast wartości. Django test client w nowszych wersjach startuje pętlę wewnętrznie, więc stringifikacja takiego proxy w szablonie (np. {{ WYDRUK_MARGINES_GORA|default:"2cm" }} w bare.html) emitowała: RuntimeWarning: Synchronous access to Constance setting 'WYDRUK_MARGINES_*' inside an async loop. Use 'await config.WYDRUK_MARGINES_*' instead. constance.utils.get_values_for_keys(keys) idzie prosto do backendu (config._backend.mget) pomijając __getattr__ — bez detekcji pętli, bez proxy, bez warningu. Działa identycznie w trybie sync i async. Dotyczyło 4 testów: - ewaluacja_optymalizacja/tests/test_discipline_pins.py ::test_reset_discipline_pins_no_unpinned_shows_warning - import_dyscyplin/tests/test_views.py ::test_CreateImport_DyscyplinView_bledny_plik ::test_CreateImport_DyscyplinView_dobry_plik ::test_UsunImport_Dyscyplin --- .../context_processors/constance_config.py | 45 ++++++++++++------- .../+constance-async-proxy.bugfix.rst | 13 ++++++ 2 files changed, 41 insertions(+), 17 deletions(-) create mode 100644 src/bpp/newsfragments/+constance-async-proxy.bugfix.rst diff --git a/src/bpp/context_processors/constance_config.py b/src/bpp/context_processors/constance_config.py index b826d4146..17b9d9eed 100644 --- a/src/bpp/context_processors/constance_config.py +++ b/src/bpp/context_processors/constance_config.py @@ -5,11 +5,37 @@ gdy constance nie jest jeszcze skonfigurowane (np. podczas migracji). """ +_CONSTANCE_KEYS = ( + "UZYWAJ_PUNKTACJI_WEWNETRZNEJ", + "POKAZUJ_INDEX_COPERNICUS", + "POKAZUJ_PUNKTACJA_SNIP", + "POKAZUJ_OSWIADCZENIE_KEN", + "SKROT_WYDZIALU_W_NAZWIE_JEDNOSTKI", + "UCZELNIA_UZYWA_WYDZIALOW", + "GOOGLE_ANALYTICS_PROPERTY_ID", + "GOOGLE_VERIFICATION_CODE", + "WYDRUK_MARGINES_GORA", + "WYDRUK_MARGINES_DOL", + "WYDRUK_MARGINES_LEWO", + "WYDRUK_MARGINES_PRAWO", +) + def constance_config(request): """ Udostępnia wybrane ustawienia z django-constance dla szablonów. + Używa ``constance.utils.get_values_for_keys`` zamiast + ``getattr(config, key)``. Powód: od constance 4.x + ``Config.__getattr__`` wykrywa aktywną pętlę asyncio (a Django + test client w nowszych wersjach startuje ją wewnętrznie) i + zwraca ``AsyncValueProxy`` — stringifikacja takiego proxy w + szablonie (``{{ VAR|default:"..." }}``) emituje + ``RuntimeWarning: Synchronous access to Constance setting '...' + inside an async loop``. ``get_values_for_keys`` idzie prosto do + backendu, bez tej detekcji, więc działa identycznie w sync i + async kontekście. + Fallback: jeżeli constance nie jest skonfigurowane, używa wartości z Django settings (ze zmiennych środowiskowych). @@ -17,24 +43,9 @@ def constance_config(request): dict: Słownik z ustawieniami dostępnymi w szablonach """ try: - from constance import config + from constance.utils import get_values_for_keys - return { - "UZYWAJ_PUNKTACJI_WEWNETRZNEJ": config.UZYWAJ_PUNKTACJI_WEWNETRZNEJ, - "POKAZUJ_INDEX_COPERNICUS": config.POKAZUJ_INDEX_COPERNICUS, - "POKAZUJ_PUNKTACJA_SNIP": config.POKAZUJ_PUNKTACJA_SNIP, - "POKAZUJ_OSWIADCZENIE_KEN": config.POKAZUJ_OSWIADCZENIE_KEN, - "SKROT_WYDZIALU_W_NAZWIE_JEDNOSTKI": ( - config.SKROT_WYDZIALU_W_NAZWIE_JEDNOSTKI - ), - "UCZELNIA_UZYWA_WYDZIALOW": config.UCZELNIA_UZYWA_WYDZIALOW, - "GOOGLE_ANALYTICS_PROPERTY_ID": config.GOOGLE_ANALYTICS_PROPERTY_ID, - "GOOGLE_VERIFICATION_CODE": config.GOOGLE_VERIFICATION_CODE, - "WYDRUK_MARGINES_GORA": config.WYDRUK_MARGINES_GORA, - "WYDRUK_MARGINES_DOL": config.WYDRUK_MARGINES_DOL, - "WYDRUK_MARGINES_LEWO": config.WYDRUK_MARGINES_LEWO, - "WYDRUK_MARGINES_PRAWO": config.WYDRUK_MARGINES_PRAWO, - } + return get_values_for_keys(_CONSTANCE_KEYS) except (ImportError, AttributeError): from django.conf import settings diff --git a/src/bpp/newsfragments/+constance-async-proxy.bugfix.rst b/src/bpp/newsfragments/+constance-async-proxy.bugfix.rst new file mode 100644 index 000000000..35d9c099d --- /dev/null +++ b/src/bpp/newsfragments/+constance-async-proxy.bugfix.rst @@ -0,0 +1,13 @@ +Context processor ``bpp.context_processors.constance_config`` używa +teraz ``constance.utils.get_values_for_keys`` zamiast +``getattr(config, key)``. Od django-constance 4.x +``Config.__getattr__`` wykrywa aktywną pętlę ``asyncio`` i zwraca +``AsyncValueProxy`` zamiast bezpośredniej wartości. Django test +client w nowszych wersjach startuje pętlę wewnętrznie, więc w +testach (i faktycznie w ASGI-runtime) szablony renderujące +``{{ WYDRUK_MARGINES_GORA|default:"2cm" }}`` emitowały +``RuntimeWarning: Synchronous access to Constance setting +'WYDRUK_MARGINES_*' inside an async loop``. +``get_values_for_keys`` idzie prosto do backendu, bez detekcji +pętli, więc działa identycznie w obu kontekstach i nie odpala +warningu. From 534a3dd6af01c8d2a3cfb13decb5024787e8253e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 19:46:30 +0200 Subject: [PATCH 12/15] test(pbn): wrap paged-endpoint mock data in pbn_pageable_json MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Produkcja — `search_publications`, `get_institution_publication_v2`, `get_institution_statements_of_single_publication` — uderza w paginowane endpointy PBN i używa `post_pages`/`get_pages`, czyli oczekuje odpowiedzi o strukturze `{content, pageable, number, totalElements, totalPages, ...}`. Te funkcje są POPRAWNE. Mocki w testach zwracały płaską listę (albo `[]`), co w `transport._pages` odpalało RuntimeWarning: PBNClient.{get,post}_page request for ... did not return a paged resource, maybe use PBNClient.{get,post} (without 'page') instead Warning dotyczył 4 plików testowych (test_client_sync, test_client_helpers, test_bpp_admin_helpers, test_views/test_api). Owinąłem ok. 30 mocków w istniejący już helper `fixtures.pbn_api.pbn_pageable_json(content)`. Produkcyjnego kodu nie ruszam — był poprawny, to mocki nie odwzorowywały rzeczywistego kształtu odpowiedzi. --- .../+pbn-test-mocks-pageable.bugfix.rst | 20 ++++ src/bpp/tests/test_views/test_api.py | 12 +- src/pbn_api/tests/test_bpp_admin_helpers.py | 16 +-- src/pbn_api/tests/test_client_helpers.py | 10 +- src/pbn_api/tests/test_client_sync.py | 104 +++++++++--------- 5 files changed, 95 insertions(+), 67 deletions(-) create mode 100644 src/bpp/newsfragments/+pbn-test-mocks-pageable.bugfix.rst diff --git a/src/bpp/newsfragments/+pbn-test-mocks-pageable.bugfix.rst b/src/bpp/newsfragments/+pbn-test-mocks-pageable.bugfix.rst new file mode 100644 index 000000000..4cda48564 --- /dev/null +++ b/src/bpp/newsfragments/+pbn-test-mocks-pageable.bugfix.rst @@ -0,0 +1,20 @@ +Mocki danych testowych PBN dla endpointów paginowanych są teraz +owinięte w ``fixtures.pbn_api.pbn_pageable_json`` — zgodnie z +rzeczywistym kształtem odpowiedzi PBN (``{content, pageable, +number, totalElements, totalPages, ...}``). Wcześniej mocki zwracały +płaską listę / pustą listę, co w +``PBNClient._pages`` triggerowało +``RuntimeWarning: PBNClient.{get,post}_page request for ... did not +return a paged resource, maybe use PBNClient.{get,post} (without +'page') instead``. Produkcyjne wywołania +(``search_publications``, ``get_institution_publication_v2``, +``get_institution_statements_of_single_publication``) pozostają bez +zmian — to są paginowane endpointy PBN, więc ``get_pages`` / +``post_pages`` są poprawne; problem był tylko w mockach. + +Poprawione pliki testowe: + +- ``src/pbn_api/tests/test_client_sync.py`` +- ``src/pbn_api/tests/test_client_helpers.py`` +- ``src/pbn_api/tests/test_bpp_admin_helpers.py`` +- ``src/bpp/tests/test_views/test_api.py`` diff --git a/src/bpp/tests/test_views/test_api.py b/src/bpp/tests/test_views/test_api.py index 8e6c767b3..20abf5ead 100644 --- a/src/bpp/tests/test_views/test_api.py +++ b/src/bpp/tests/test_views/test_api.py @@ -17,6 +17,7 @@ from bpp.views.api.pbn_get_by_parameter import GetPBNPublicationsByISBN from bpp.views.api.pubmed import GetPubmedIDView, get_data_from_ncbi from fixtures import pbn_publication_json +from fixtures.pbn_api import pbn_pageable_json def test_get_data_from_ncbi(mocker): @@ -101,7 +102,9 @@ def test_GetPBNPublicationsByISBN_jedna_praca( req = rf.post("/", data=dict(t=ISBN, rok="2021")) req.user = admin_user - pbn_client.transport.return_values["/api/v1/search/publications?size=10"] = [pub1] + pbn_client.transport.return_values[ + "/api/v1/search/publications?size=10" + ] = pbn_pageable_json([pub1]) pbn_client.transport.return_values[f"/api/v1/publications/id/{UID_REKORDU}"] = pub1 try: Uczelnia.objects.get_default = lambda *args, **kw: pbn_uczelnia @@ -139,10 +142,9 @@ def test_GetPBNPublicationsByISBN_wiele_isbn( req = rf.post("/", data=dict(t=ISBN, rok="2021")) req.user = admin_user - pbn_client.transport.return_values["/api/v1/search/publications?size=10"] = [ - pub1, - pub2, - ] + pbn_client.transport.return_values[ + "/api/v1/search/publications?size=10" + ] = pbn_pageable_json([pub1, pub2]) pbn_client.transport.return_values[f"/api/v1/publications/id/{UID_REKORDU}"] = pub1 pbn_client.transport.return_values[f"/api/v1/publications/id/{UID_REKORDU}2"] = pub2 diff --git a/src/pbn_api/tests/test_bpp_admin_helpers.py b/src/pbn_api/tests/test_bpp_admin_helpers.py index 8c92616c3..f52b7a4a8 100644 --- a/src/pbn_api/tests/test_bpp_admin_helpers.py +++ b/src/pbn_api/tests/test_bpp_admin_helpers.py @@ -5,7 +5,7 @@ from bpp.admin.helpers.pbn_api.gui import sprobuj_wyslac_do_pbn_gui from bpp.models import Charakter_Formalny, Wydawnictwo_Ciagle from fixtures import MOCK_MONGO_ID, MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA -from fixtures.pbn_api import MOCK_RETURNED_MONGODB_DATA +from fixtures.pbn_api import MOCK_RETURNED_MONGODB_DATA, pbn_pageable_json from pbn_api.adapters.wydawnictwo import WydawnictwoPBNAdapter from pbn_api.client import ( PBN_GET_INSTITUTION_STATEMENTS, @@ -190,10 +190,10 @@ def test_sprobuj_wyslac_do_pbn_z_oswiadczeniami( ] = MOCK_RETURNED_MONGODB_DATA pbn_client.transport.return_values[ PBN_GET_INSTITUTION_STATEMENTS + "?publicationId=123&size=5120" - ] = [] + ] = pbn_pageable_json([]) pbn_client.transport.return_values[ PBN_GET_INSTITUTION_PUBLICATIONS_V2 + "?publicationId=123&size=10" - ] = MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA + ] = pbn_pageable_json(MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA) with middleware(req): sprobuj_wyslac_do_pbn_gui( @@ -253,7 +253,7 @@ def test_sprobuj_wyslac_do_pbn_ostrzezenie_brak_dyscypliny_autora( ] = MOCK_RETURNED_MONGODB_DATA pbn_client.transport.return_values[ PBN_GET_INSTITUTION_STATEMENTS + "?publicationId=123&size=5120" - ] = [] + ] = pbn_pageable_json([]) with middleware(req): sprobuj_wyslac_do_pbn_gui( @@ -301,10 +301,10 @@ def test_sprobuj_wyslac_do_pbn_przychodzi_istniejacy_pbn_uid_dla_nowego_rekordu( ) pbn_client.transport.return_values[ PBN_GET_INSTITUTION_STATEMENTS + f"?publicationId={MOCK_MONGO_ID}&size=5120" - ] = [] + ] = pbn_pageable_json([]) pbn_client.transport.return_values[ PBN_GET_INSTITUTION_PUBLICATIONS_V2 + "?publicationId=123&size=10" - ] = MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA + ] = pbn_pageable_json(MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA) with middleware(req): sprobuj_wyslac_do_pbn_gui( @@ -350,10 +350,10 @@ def test_sprobuj_wyslac_do_pbn_przychodzi_inny_pbn_uid_dla_starego_rekordu( ) pbn_client.transport.return_values[ PBN_GET_INSTITUTION_STATEMENTS + f"?publicationId={MOCK_MONGO_ID}&size=5120" - ] = [] + ] = pbn_pageable_json([]) pbn_client.transport.return_values[ PBN_GET_INSTITUTION_PUBLICATIONS_V2 + "?publicationId=123123&size=10" - ] = MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA + ] = pbn_pageable_json(MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA) with middleware(req): sprobuj_wyslac_do_pbn_gui( diff --git a/src/pbn_api/tests/test_client_helpers.py b/src/pbn_api/tests/test_client_helpers.py index cef035389..0e40a4e81 100644 --- a/src/pbn_api/tests/test_client_helpers.py +++ b/src/pbn_api/tests/test_client_helpers.py @@ -12,7 +12,7 @@ from bpp.admin.helpers.pbn_api.gui import sprobuj_wyslac_do_pbn_gui from fixtures import MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA -from fixtures.pbn_api import MOCK_RETURNED_MONGODB_DATA +from fixtures.pbn_api import MOCK_RETURNED_MONGODB_DATA, pbn_pageable_json from pbn_api.client import ( PBN_GET_INSTITUTION_STATEMENTS, PBN_GET_PUBLICATION_BY_ID_URL, @@ -78,14 +78,14 @@ def test_helpers_wysylka_z_uid_uczelni( pbn_client.transport.return_values[ PBN_GET_INSTITUTION_PUBLICATIONS_V2 + f"?publicationId={pbn_wydawnictwo_zwarte_z_autorem_z_dyscyplina.pk}&size=10" - ] = MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA + ] = pbn_pageable_json(MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA) pbn_client.transport.return_values[PBN_GET_PUBLICATION_BY_ID_URL.format(id=456)] = ( MOCK_RETURNED_MONGODB_DATA ) pbn_client.transport.return_values[ PBN_GET_INSTITUTION_STATEMENTS + "?publicationId=123&size=5120" - ] = [] + ] = pbn_pageable_json([]) req = rf.get("/") req._uczelnia = pbn_uczelnia @@ -141,11 +141,11 @@ def test_helpers_wysylka_bez_uid_uczelni( pbn_client.transport.return_values[ PBN_GET_INSTITUTION_PUBLICATIONS_V2 + f"?publicationId={pbn_wydawnictwo_zwarte_z_autorem_z_dyscyplina.pk}&size=10" - ] = MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA + ] = pbn_pageable_json(MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA) pbn_client.transport.return_values[ PBN_GET_INSTITUTION_STATEMENTS + "?publicationId=123&size=5120" - ] = [] + ] = pbn_pageable_json([]) req = rf.get("/") req._uczelnia = pbn_uczelnia diff --git a/src/pbn_api/tests/test_client_sync.py b/src/pbn_api/tests/test_client_sync.py index 3a9e30c9f..6a42b96b6 100644 --- a/src/pbn_api/tests/test_client_sync.py +++ b/src/pbn_api/tests/test_client_sync.py @@ -10,7 +10,7 @@ from bpp.decorators import json from fixtures import MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA -from fixtures.pbn_api import MOCK_RETURNED_MONGODB_DATA +from fixtures.pbn_api import MOCK_RETURNED_MONGODB_DATA, pbn_pageable_json from pbn_api.client import ( PBN_DELETE_PUBLICATION_STATEMENT, PBN_GET_INSTITUTION_STATEMENTS, @@ -43,25 +43,27 @@ def test_sync_publication_to_samo_id( ] = MOCK_RETURNED_MONGODB_DATA pbn_client.transport.return_values[ PBN_GET_INSTITUTION_PUBLICATIONS_V2 + "?publicationId=123&size=10" - ] = MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA + ] = pbn_pageable_json(MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA) pbn_client.transport.return_values[PBN_GET_PUBLICATION_BY_ID_URL.format(id=456)] = ( MOCK_RETURNED_MONGODB_DATA ) pbn_client.transport.return_values[ PBN_GET_INSTITUTION_STATEMENTS + "?publicationId=123&size=5120" - ] = [ - { - "id": "eaec3254-2eb1-44d9-8c3c-e68fc2a48bd9", - "addedTimestamp": "2020.05.06", - "institutionId": pbn_jednostka.pbn_uid_id, - "personId": pbn_autor.pbn_uid_id, - "publicationId": pbn_wydawnictwo_zwarte_z_autorem_z_dyscyplina.pbn_uid_id, - "area": "200", - "inOrcid": True, - "type": "FOOBAR", - } - ] + ] = pbn_pageable_json( + [ + { + "id": "eaec3254-2eb1-44d9-8c3c-e68fc2a48bd9", + "addedTimestamp": "2020.05.06", + "institutionId": pbn_jednostka.pbn_uid_id, + "personId": pbn_autor.pbn_uid_id, + "publicationId": pbn_wydawnictwo_zwarte_z_autorem_z_dyscyplina.pbn_uid_id, + "area": "200", + "inOrcid": True, + "type": "FOOBAR", + } + ] + ) pbn_client.sync_publication(pbn_wydawnictwo_zwarte_z_autorem_z_dyscyplina) @@ -86,11 +88,11 @@ def test_sync_publication_tekstowo_podane_id( ) pbn_client.transport.return_values[ PBN_GET_INSTITUTION_PUBLICATIONS_V2 + "?publicationId=123&size=10" - ] = MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA + ] = pbn_pageable_json(MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA) pbn_client.transport.return_values[ PBN_GET_INSTITUTION_STATEMENTS + "?publicationId=123&size=5120" - ] = [] + ] = pbn_pageable_json([]) pbn_client.sync_publication( f"wydawnictwo_zwarte:{pbn_wydawnictwo_zwarte_z_autorem_z_dyscyplina.pk}" @@ -116,13 +118,13 @@ def test_sync_publication_nowe_id( ] = MOCK_RETURNED_MONGODB_DATA pbn_client.transport.return_values[ PBN_GET_INSTITUTION_STATEMENTS + "?publicationId=123&size=5120" - ] = [] + ] = pbn_pageable_json([]) pbn_client.transport.return_values[PBN_GET_PUBLICATION_BY_ID_URL.format(id=456)] = ( MOCK_RETURNED_MONGODB_DATA ) pbn_client.transport.return_values[ PBN_GET_INSTITUTION_PUBLICATIONS_V2 + "?publicationId=123&size=10" - ] = MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA + ] = pbn_pageable_json(MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA) pbn_client.sync_publication(pbn_wydawnictwo_zwarte_z_autorem_z_dyscyplina) @@ -152,17 +154,17 @@ def test_sync_publication_wysylka_z_zerowym_pk( ] = MOCK_RETURNED_MONGODB_DATA pbn_client.transport.return_values[ PBN_GET_INSTITUTION_PUBLICATIONS_V2 + "?publicationId=123&size=10" - ] = MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA + ] = pbn_pageable_json(MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA) pbn_client.transport.return_values[PBN_GET_PUBLICATION_BY_ID_URL.format(id=456)] = ( MOCK_RETURNED_MONGODB_DATA ) pbn_client.transport.return_values[ PBN_GET_INSTITUTION_STATEMENTS + "?publicationId=123&size=5120" - ] = [] + ] = pbn_pageable_json([]) pbn_client.transport.return_values[ PBN_GET_INSTITUTION_PUBLICATIONS_V2 + "?publicationId=123&size=10" - ] = MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA + ] = pbn_pageable_json(MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA) # To pójdzie pbn_client.sync_publication( @@ -197,7 +199,7 @@ def test_sync_publication_kasuj_oswiadczenia_przed_wszystko_dobrze( ] = MOCK_RETURNED_MONGODB_DATA pbn_client.transport.return_values[ PBN_GET_INSTITUTION_PUBLICATIONS_V2 + "?publicationId=123&size=10" - ] = MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA + ] = pbn_pageable_json(MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA) pbn_client.transport.return_values[PBN_GET_PUBLICATION_BY_ID_URL.format(id=456)] = ( MOCK_RETURNED_MONGODB_DATA ) @@ -207,18 +209,20 @@ def test_sync_publication_kasuj_oswiadczenia_przed_wszystko_dobrze( ] = [] pbn_client.transport.return_values[ PBN_GET_INSTITUTION_STATEMENTS + "?publicationId=123&size=5120" - ] = [ - { - "id": "eaec3254-2eb1-44d9-8c3c-e68fc2a48bd9", - "addedTimestamp": "2020.05.06", - "institutionId": pbn_jednostka.pbn_uid_id, - "personId": pbn_autor.pbn_uid_id, - "publicationId": pbn_wydawnictwo_zwarte_z_autorem_z_dyscyplina.pbn_uid_id, - "area": "200", - "inOrcid": True, - "type": "FOOBAR", - } - ] + ] = pbn_pageable_json( + [ + { + "id": "eaec3254-2eb1-44d9-8c3c-e68fc2a48bd9", + "addedTimestamp": "2020.05.06", + "institutionId": pbn_jednostka.pbn_uid_id, + "personId": pbn_autor.pbn_uid_id, + "publicationId": pbn_wydawnictwo_zwarte_z_autorem_z_dyscyplina.pbn_uid_id, + "area": "200", + "inOrcid": True, + "type": "FOOBAR", + } + ] + ) pbn_client.sync_publication( pbn_wydawnictwo_zwarte_z_autorem_z_dyscyplina, @@ -252,7 +256,7 @@ def test_sync_publication_kasuj_oswiadczenia_przed_blad_400_nie_zaburzy( pbn_client.transport.return_values[ PBN_GET_INSTITUTION_PUBLICATIONS_V2 + f"?publicationId={pbn_publication.pk}&size=10" - ] = MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA + ] = pbn_pageable_json(MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA) pbn_client.transport.return_values[PBN_GET_PUBLICATION_BY_ID_URL.format(id=456)] = ( MOCK_RETURNED_MONGODB_DATA ) @@ -274,18 +278,20 @@ def test_sync_publication_kasuj_oswiadczenia_przed_blad_400_nie_zaburzy( pbn_client.transport.return_values[ PBN_GET_INSTITUTION_STATEMENTS + "?publicationId=123&size=5120" - ] = [ - { - "id": "eaec3254-2eb1-44d9-8c3c-e68fc2a48bd9", - "addedTimestamp": "2020.05.06", - "institutionId": pbn_jednostka.pbn_uid_id, - "personId": pbn_autor.pbn_uid_id, - "publicationId": pbn_wydawnictwo_zwarte_z_autorem_z_dyscyplina.pbn_uid_id, - "area": "200", - "inOrcid": True, - "type": "FOOBAR", - } - ] + ] = pbn_pageable_json( + [ + { + "id": "eaec3254-2eb1-44d9-8c3c-e68fc2a48bd9", + "addedTimestamp": "2020.05.06", + "institutionId": pbn_jednostka.pbn_uid_id, + "personId": pbn_autor.pbn_uid_id, + "publicationId": pbn_wydawnictwo_zwarte_z_autorem_z_dyscyplina.pbn_uid_id, + "area": "200", + "inOrcid": True, + "type": "FOOBAR", + } + ] + ) pbn_client.sync_publication( pbn_wydawnictwo_zwarte_z_autorem_z_dyscyplina, @@ -334,12 +340,12 @@ def test_upload_and_sync_publication_without_existing_publication( # Mock empty statements response pbn_client.transport.return_values[ PBN_GET_INSTITUTION_STATEMENTS + f"?publicationId={new_object_id}&size=5120" - ] = [] + ] = pbn_pageable_json([]) # Mock institution publications v2 response pbn_client.transport.return_values[ PBN_GET_INSTITUTION_PUBLICATIONS_V2 + f"?publicationId={new_object_id}&size=10" - ] = MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA + ] = pbn_pageable_json(MOCK_RETURNED_INSTITUTION_PUBLICATION_V2_DATA) # Call sync_publication() - this internally calls upload_publication() # which should succeed without FK error, then download_publication() From 663e22a134616f3edd8c7f2e4251845a07c57e8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 19:58:53 +0200 Subject: [PATCH 13/15] fix(pytest): restore pytest_plugins, avoid eager fixture-module imports MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit W poprzednim refaktorze (commit 057f291a "Duże pliki na małe") deklaracja `pytest_plugins` została USUNIĘTA z `src/fixtures/conftest.py` z notatką "moved to top-level conftest.py (per pytest requirements)" — ale nigdy nie została do top-level conftest dodana. Dodatkowo `fixtures/__init__.py` robiło `from .conftest_X import *` dla wszystkich 5 modułów, więc każdy `from fixtures import …` (np. `from fixtures.playwright_fixtures import …` w `src/conftest.py:419`) pociągał je w całości PRZED pytestem. Przy rejestracji jako pluginy pytest już widział je w sys.modules → 85 ostrzeżeń: PytestAssertRewriteWarning: Module already imported so cannot be rewritten; fixtures.conftest_{models,publications,system,browser, disciplines} Fix: - Dodaję `pytest_plugins = [...]` w top-level `src/conftest.py` (5 modułów conftest_*). Kolejność deklaracji wewnątrz modułu nie ma znaczenia — pytest czyta atrybut po pełnym załadowaniu. - `fixtures/__init__.py` NIE importuje już conftest_*, tylko `fixtures.const`, `fixtures.pbn_api`, `fixtures.wydawnictwa` — moduły, które nie są pytest plugins, więc eager import ich nie psuje. - Stałe modułowe `NORMAL_DJANGO_USER_{LOGIN,PASSWORD}` i `JEDNOSTKA_{UCZELNI,PODRZEDNA}` przeniesione z conftest_browser / conftest_models do nowego `fixtures/const.py`. conftest_X importuje je stamtąd; `fixtures/__init__.py` re-eksportuje przez `from .const import *`. Jednocześnie naprawiam regresję z commita `1a372897` (grupa [3] — UnorderedObjectListWarning): `.order_by("nazwa", "pk")` na `QuerySetSequence` w `ZrodloAutocomplete.get_queryset()` propagowało się do sliced sub-queryset (`qs.filter(...)[:10]`), co od Django 4.x zgłasza `TypeError: Cannot reorder a query once a slice has been taken`. Bazowy queryset `_get_base_queryset` już ma .order_by, więc każdy slice ma porządek — wystarcza; kolejność między gałęziami PBN jest priorytetowa (intencjonalnie, nie alfabetyczna). --- ...+pytest-assert-rewrite-fixtures.bugfix.rst | 19 +++++++++++++++++++ src/bpp/views/autocomplete/simple.py | 12 ++++++++++-- src/conftest.py | 18 ++++++++++++++++++ src/fixtures/__init__.py | 19 +++++++++++++------ src/fixtures/conftest_browser.py | 3 +-- src/fixtures/conftest_models.py | 8 ++------ src/fixtures/const.py | 17 +++++++++++++++++ 7 files changed, 80 insertions(+), 16 deletions(-) create mode 100644 src/bpp/newsfragments/+pytest-assert-rewrite-fixtures.bugfix.rst create mode 100644 src/fixtures/const.py diff --git a/src/bpp/newsfragments/+pytest-assert-rewrite-fixtures.bugfix.rst b/src/bpp/newsfragments/+pytest-assert-rewrite-fixtures.bugfix.rst new file mode 100644 index 000000000..ad9513031 --- /dev/null +++ b/src/bpp/newsfragments/+pytest-assert-rewrite-fixtures.bugfix.rst @@ -0,0 +1,19 @@ +Pytest nie emituje już ostrzeżeń ``PytestAssertRewriteWarning: +Module already imported so cannot be rewritten; fixtures.conftest_*`` +(85 wystąpień w poprzednim runie). Przywrócono deklarację +``pytest_plugins = [...]`` w top-level ``src/conftest.py`` — pytest +rejestruje ``fixtures.conftest_{models,publications,system,browser, +disciplines}`` jako pluginy z aplikowanym assert-rewritingiem +przed ich pierwszym importem. + +Jednocześnie ``fixtures/__init__.py`` przestał eager-importować +``conftest_*`` — wcześniejsze ``from .conftest_X import *`` +pociągało te moduły przez łańcuch +``from fixtures.playwright_fixtures import ...`` → ``fixtures/ +__init__.py`` PRZED rejestracją jako plugin, co właśnie generowało +ostrzeżenia. + +Stałe (``NORMAL_DJANGO_USER_LOGIN/PASSWORD``, ``JEDNOSTKA_UCZELNI``, +``JEDNOSTKA_PODRZEDNA``) przeniesione do nowego modułu +``fixtures.const``, żeby ``from fixtures import X`` mogło je +re-eksportować bez ściągania modułów-pluginów. diff --git a/src/bpp/views/autocomplete/simple.py b/src/bpp/views/autocomplete/simple.py index 199889b95..0d2b60c71 100644 --- a/src/bpp/views/autocomplete/simple.py +++ b/src/bpp/views/autocomplete/simple.py @@ -257,10 +257,18 @@ def get_queryset(self): )[:10] qs_without_pbn = qs.filter(pbn_uid__isnull=True)[:10] - # Use QuerySetSequence to chain querysets with priority + # Use QuerySetSequence to chain querysets with priority. + # UWAGA: nie wołamy tu .order_by() na sekwencji, bo + # sub-querysety są już sliced (`[:10]`), a QuerySetSequence + # propaguje order_by do każdego z nich — a Django od 4.x + # rzuca "Cannot reorder a query once a slice has been taken". + # Bazowy qs ma już .order_by("nazwa", "pk") z + # _get_base_queryset, więc każde filtrowane `[:10]` zachowuje + # porządek; jedynie kolejność między trzema gałęziami PBN + # jest priorytetowa (celowo, nie alfabetyczna). res = QuerySetSequence( qs_with_full_pbn, qs_with_pbn_no_mnisw, qs_without_pbn - ).order_by("nazwa", "pk") + ) res.model = Zrodlo # django-autocomplete-light needs this return res diff --git a/src/conftest.py b/src/conftest.py index f8a4a3f1f..caf1f4fb5 100644 --- a/src/conftest.py +++ b/src/conftest.py @@ -20,6 +20,24 @@ ) from channels_live_server import channels_live_server # noqa: F401 +# pytest_plugins: rejestrujemy fixture'owe moduły jako pluginy, żeby +# pytest zastosował assert-rewriting zanim je zaimportuje. Bez tej +# deklaracji było 85+ ostrzeżeń ``PytestAssertRewriteWarning: Module +# already imported so cannot be rewritten; fixtures.conftest_*``. +# Kolejność wewnątrz conftest nie ma znaczenia — pytest czyta atrybut +# po pełnym załadowaniu modułu; ważne, żeby +# ``fixtures/__init__.py`` NIE importował tych modułów eager +# (`from .conftest_X import *`), inaczej trafiają do ``sys.modules`` +# przed rejestracją. +pytest_plugins = [ + "fixtures.conftest", + "fixtures.conftest_models", + "fixtures.conftest_publications", + "fixtures.conftest_system", + "fixtures.conftest_browser", + "fixtures.conftest_disciplines", +] + # Baseline test-DB monkey-patch is installed by # ``django_pg_baseline.apps.DjangoPgBaselineConfig.ready()`` (the app # lives in INSTALLED_APPS + settings.PG_BASELINE). diff --git a/src/fixtures/__init__.py b/src/fixtures/__init__.py index c91b250b8..aa90b4ce0 100644 --- a/src/fixtures/__init__.py +++ b/src/fixtures/__init__.py @@ -1,8 +1,15 @@ -from .conftest import * # noqa -from .conftest_browser import * # noqa -from .conftest_models import * # noqa -from .conftest_publications import * # noqa -from .conftest_system import * # noqa -from .conftest_disciplines import * # noqa +"""Public symbols re-eksportowane z ``fixtures`` do użycia w testach. + +Nie importujemy tu ``conftest_*`` — zostały zarejestrowane jako +pytest plugins w ``src/conftest.py`` (``pytest_plugins = [...]``), +a eager import pociągałby je PRZED rejestracją przez pytest, +skutkując ``PytestAssertRewriteWarning: Module already imported so +cannot be rewritten``. + +Stałe (``NORMAL_DJANGO_USER_*``, ``JEDNOSTKA_*``) trzymamy w +``fixtures.const`` — oddzielonym od modułów-pluginów. +""" + +from .const import * # noqa from .pbn_api import * # noqa from .wydawnictwa import * # noqa diff --git a/src/fixtures/conftest_browser.py b/src/fixtures/conftest_browser.py index acff21444..d89c0434a 100644 --- a/src/fixtures/conftest_browser.py +++ b/src/fixtures/conftest_browser.py @@ -13,8 +13,7 @@ import django_webtest import webtest -NORMAL_DJANGO_USER_LOGIN = "test_login_bpp" -NORMAL_DJANGO_USER_PASSWORD = "test_password" +from .const import NORMAL_DJANGO_USER_LOGIN, NORMAL_DJANGO_USER_PASSWORD @pytest.fixture diff --git a/src/fixtures/conftest_models.py b/src/fixtures/conftest_models.py index 7551d13bf..b2d39436a 100644 --- a/src/fixtures/conftest_models.py +++ b/src/fixtures/conftest_models.py @@ -9,6 +9,8 @@ from bpp.models.struktura import Jednostka, Uczelnia, Wydzial from bpp.models.zrodlo import Zrodlo +from .const import JEDNOSTKA_PODRZEDNA, JEDNOSTKA_UCZELNI + def current_rok(): return datetime.now().date().year @@ -86,9 +88,6 @@ def _jednostka_maker(nazwa, skrot, wydzial, **kwargs): return ret -JEDNOSTKA_UCZELNI = "Jednostka Uczelni" - - @pytest.fixture(scope="function") def jednostka(wydzial, db): return _jednostka_maker(JEDNOSTKA_UCZELNI, skrot="Jedn. Ucz.", wydzial=wydzial) @@ -122,9 +121,6 @@ def druga_aktualna_jednostka(druga_jednostka, drugi_wydzial): return druga_jednostka -JEDNOSTKA_PODRZEDNA = "Jednostka P-rzedna" - - @pytest.fixture(scope="function") def jednostka_podrzedna(jednostka): return _jednostka_maker( diff --git a/src/fixtures/const.py b/src/fixtures/const.py new file mode 100644 index 000000000..ba4fcb9ad --- /dev/null +++ b/src/fixtures/const.py @@ -0,0 +1,17 @@ +"""Stałe używane przez fixtury oraz testy. + +Moduł jest celowo odseparowany od ``fixtures.conftest_*``, żeby +``fixtures/__init__.py`` mógł re-eksportować stałe bez ściągania +za sobą modułów zadeklarowanych w ``pytest_plugins`` w +``src/conftest.py``. W przeciwnym razie import stałych typu +``from fixtures import NORMAL_DJANGO_USER_LOGIN`` pociągałby za +sobą ``conftest_browser`` PRZED rejestracją go przez pytest jako +plugin i odpalał ``PytestAssertRewriteWarning: Module already +imported so cannot be rewritten``. +""" + +NORMAL_DJANGO_USER_LOGIN = "test_login_bpp" +NORMAL_DJANGO_USER_PASSWORD = "test_password" + +JEDNOSTKA_UCZELNI = "Jednostka Uczelni" +JEDNOSTKA_PODRZEDNA = "Jednostka P-rzedna" From b518f462988d1b872acc39d92da472c90e123d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 20:40:19 +0200 Subject: [PATCH 14/15] chore(deps): bump MOAI-iplweb 2.0.0 -> >=2.0.1, add pytest filters for pyoai/webtest MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit MOAI-iplweb 2.0.1 (upstream fork by same author) naprawia datetime.utcnow() -> datetime.now(UTC). Zniknął warning z moai/oai.py. Downgrade tranzytywny sqlalchemy 2.x -> 1.4 i setuptools 80 -> 79 wymuszony przez moai-iplweb 2.0.1 constraints (sqlalchemy<2, setuptools<80). Dla dwóch pozostałych zewnętrznych warningów bez lokalnego forka (grupy [10] i [11] TODO) dodałem targetowane filtry w pytest.ini: pyoai 2.5.0 (oaipmh.server) - datetime.utcnow, webtest 3.0.7 (webtest.forms) - bs4.findAll. Zgłoszenia upstream w toku. --- pyproject.toml | 2 +- pytest.ini | 6 ++ .../+moai-bump-and-pytest-filters.bugfix.rst | 17 ++++ uv.lock | 83 +++++++------------ 4 files changed, 52 insertions(+), 56 deletions(-) create mode 100644 src/bpp/newsfragments/+moai-bump-and-pytest-filters.bugfix.rst diff --git a/pyproject.toml b/pyproject.toml index e4aa263b2..617c977f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -62,7 +62,7 @@ dependencies = [ "django-fsm>=3,<4", "django-mptt>=0.16,<1", "wosclient==0.1.5", - "MOAI-iplweb==2.0.0", + "MOAI-iplweb>=2.0.1", "django_redis==5.3.0", "django-filter>=25.1,<25.2", "dbfread>=2.0.7", diff --git a/pytest.ini b/pytest.ini index 9878f1bde..2d7d40ba3 100644 --- a/pytest.ini +++ b/pytest.ini @@ -41,6 +41,12 @@ filterwarnings = # FORMS_URLFIELD_ASSUME_HTTPS is an intentional opt-in for Django 6.0 # behaviour; to be removed together with the setting on 6.0 upgrade. ignore:.*FORMS_URLFIELD_ASSUME_HTTPS.*:DeprecationWarning + # pyoai 2.5.0 (infrae/pyoai) nadal woła datetime.utcnow() + # w oaipmh/server.py; nie mamy forku — zgłoszenie upstream. + ignore:.*datetime\.datetime\.utcnow.*:DeprecationWarning:oaipmh\.server + # webtest 3.0.7 nadal używa bs4.findAll w forms.py:436 (zamiast + # find_all); nie mamy forku — zgłoszenie upstream (Pylons/webtest). + ignore:.*findAll.*:DeprecationWarning:webtest\.forms norecursedirs = src/bpp/media/ .worktrees diff --git a/src/bpp/newsfragments/+moai-bump-and-pytest-filters.bugfix.rst b/src/bpp/newsfragments/+moai-bump-and-pytest-filters.bugfix.rst new file mode 100644 index 000000000..0b5ca9f9d --- /dev/null +++ b/src/bpp/newsfragments/+moai-bump-and-pytest-filters.bugfix.rst @@ -0,0 +1,17 @@ +Zbumpowano ``MOAI-iplweb`` z ``==2.0.0`` do ``>=2.0.1`` (release +2.0.1 zawiera fix ``datetime.utcnow()`` → ``datetime.now(UTC)``). +Zniknęły ostrzeżenia ``DeprecationWarning`` z ``moai/oai.py``. + +Dodano dwa targetowane filtry w ``pytest.ini`` dla pozostałych +zewnętrznych warningów, których nie mamy gdzie naprawić w bpp: + +- ``oaipmh.server`` (paczka ``pyoai`` 2.5.0) — wywołuje + ``datetime.utcnow()``; nie mamy forka, zgłoszenie upstream + w toku. +- ``webtest.forms`` (paczka ``webtest`` 3.0.7) — używa + ``bs4.findAll`` zamiast ``find_all``; nie mamy forka, zgłoszenie + do ``Pylons/webtest`` w toku. + +Zmienione zależności tranzytywne (uv downgrade wymuszone przez +moai-iplweb ``sqlalchemy<2`` i ``setuptools<80``): ``sqlalchemy +2.0.44 → 1.4.54``, ``setuptools 80.9 → 79.0``. diff --git a/uv.lock b/uv.lock index 2ae382848..7b415ab89 100644 --- a/uv.lock +++ b/uv.lock @@ -475,7 +475,7 @@ requires-dist = [ { name = "isbnlib", specifier = ">=3.10.14" }, { name = "langdetect", specifier = ">=1.0.9" }, { name = "markdown", specifier = ">=3.7,<4" }, - { name = "moai-iplweb", specifier = "==2.0.0" }, + { name = "moai-iplweb", specifier = ">=2.0.1" }, { name = "model-bakery", marker = "extra == 'dev'", specifier = ">=1.23.4" }, { name = "nest-asyncio", marker = "extra == 'dev'", specifier = ">=1.6.0" }, { name = "nh3", specifier = ">=0.3.4" }, @@ -2385,15 +2385,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d5/08/c2409cb01d5368dcfedcbaffa7d044cc8957d57a9d0855244a5eb4709d30/funcy-2.0-py2.py3-none-any.whl", hash = "sha256:53df23c8bb1651b12f095df764bfb057935d49537a56de211b098f4c79614bb0", size = 30891, upload-time = "2023-03-28T06:22:42.576Z" }, ] -[[package]] -name = "future" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, -] - [[package]] name = "greenlet" version = "3.2.4" @@ -3298,23 +3289,22 @@ wheels = [ [[package]] name = "moai-iplweb" -version = "2.0.0" +version = "2.0.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "future", marker = "platform_python_implementation != 'PyPy'" }, { name = "paste", marker = "platform_python_implementation != 'PyPy'" }, { name = "pastedeploy", marker = "platform_python_implementation != 'PyPy'" }, { name = "pastescript", marker = "platform_python_implementation != 'PyPy'" }, { name = "pyoai", marker = "platform_python_implementation != 'PyPy'" }, - { name = "six", marker = "platform_python_implementation != 'PyPy'" }, + { name = "setuptools", marker = "platform_python_implementation != 'PyPy'" }, { name = "sqlalchemy", marker = "platform_python_implementation != 'PyPy'" }, { name = "webob", marker = "platform_python_implementation != 'PyPy'" }, { name = "wsgi-intercept", marker = "platform_python_implementation != 'PyPy'" }, { name = "wsgiutils", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e6/e7/40126fe3a8edfc840ec7e2cf9ad65b2b595a470bbec2be1216feba8b181b/MOAI-iplweb-2.0.0.tar.gz", hash = "sha256:1c969cae673272bad6f0ac70f547e52a65343e8978780885d255ae56e3ab2c66", size = 47327, upload-time = "2018-07-14T16:18:17.407Z" } +sdist = { url = "https://files.pythonhosted.org/packages/33/57/771558d6cbd4868652d0284f0e36bed7f408a34c3e794a58da294cf7a7ff/moai_iplweb-2.0.1.tar.gz", hash = "sha256:31266801424410994731cee79f897179ed65881948646bd4a6529cde1351e1ad", size = 36244, upload-time = "2026-04-16T12:32:47.898Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/52/cdb40b8e40f3195623bfc264b16fca6361dea7add891c276db8263b464e0/MOAI_iplweb-2.0.0-py3-none-any.whl", hash = "sha256:f4eb653b19b9e14e4bed281b4dad59a2cadf5bee57f6ae9033fbe3b54d7bccd2", size = 41633, upload-time = "2018-07-14T16:18:19.642Z" }, + { url = "https://files.pythonhosted.org/packages/e3/98/8ef9f7ab6af9a06209dc8e0228b4fbf7418051ef49d613aa6db5b81df47d/moai_iplweb-2.0.1-py3-none-any.whl", hash = "sha256:396a954780c9bb6a566dc24446eb8e98bcdb479bea9b8eb7978455f7d5db06b5", size = 43830, upload-time = "2026-04-16T12:32:46.335Z" }, ] [[package]] @@ -5415,11 +5405,11 @@ wheels = [ [[package]] name = "setuptools" -version = "80.9.0" +version = "79.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/71/b6365e6325b3290e14957b2c3a804a529968c77a049b2ed40c095f749707/setuptools-79.0.1.tar.gz", hash = "sha256:128ce7b8f33c3079fd1b067ecbb4051a66e8526e7b65f6cec075dfc650ddfa88", size = 1367909, upload-time = "2025-04-23T22:20:59.241Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, + { url = "https://files.pythonhosted.org/packages/0d/6d/b4752b044bf94cb802d88a888dc7d288baaf77d7910b7dedda74b5ceea0c/setuptools-79.0.1-py3-none-any.whl", hash = "sha256:e147c0549f27767ba362f9da434eab9c5dc0045d5304feb602a0af001089fc51", size = 1256281, upload-time = "2025-04-23T22:20:56.768Z" }, ] [[package]] @@ -5648,47 +5638,30 @@ wheels = [ [[package]] name = "sqlalchemy" -version = "2.0.44" +version = "1.4.54" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "greenlet", marker = "(platform_machine == 'AMD64' and platform_python_implementation != 'PyPy') or (platform_machine == 'WIN32' and platform_python_implementation != 'PyPy') or (platform_machine == 'aarch64' and platform_python_implementation != 'PyPy') or (platform_machine == 'amd64' and platform_python_implementation != 'PyPy') or (platform_machine == 'ppc64le' and platform_python_implementation != 'PyPy') or (platform_machine == 'win32' and platform_python_implementation != 'PyPy') or (platform_machine == 'x86_64' and platform_python_implementation != 'PyPy')" }, - { name = "typing-extensions", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f0/f2/840d7b9496825333f532d2e3976b8eadbf52034178aac53630d09fe6e1ef/sqlalchemy-2.0.44.tar.gz", hash = "sha256:0ae7454e1ab1d780aee69fd2aae7d6b8670a581d8847f2d1e0f7ddfbf47e5a22", size = 9819830, upload-time = "2025-10-10T14:39:12.935Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a2/a7/e9ccfa7eecaf34c6f57d8cb0bb7cbdeeff27017cc0f5d0ca90fdde7a7c0d/sqlalchemy-2.0.44-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7c77f3080674fc529b1bd99489378c7f63fcb4ba7f8322b79732e0258f0ea3ce", size = 2137282, upload-time = "2025-10-10T15:36:10.965Z" }, - { url = "https://files.pythonhosted.org/packages/b1/e1/50bc121885bdf10833a4f65ecbe9fe229a3215f4d65a58da8a181734cae3/sqlalchemy-2.0.44-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4c26ef74ba842d61635b0152763d057c8d48215d5be9bb8b7604116a059e9985", size = 2127322, upload-time = "2025-10-10T15:36:12.428Z" }, - { url = "https://files.pythonhosted.org/packages/46/f2/a8573b7230a3ce5ee4b961a2d510d71b43872513647398e595b744344664/sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4a172b31785e2f00780eccab00bc240ccdbfdb8345f1e6063175b3ff12ad1b0", size = 3214772, upload-time = "2025-10-10T15:34:15.09Z" }, - { url = "https://files.pythonhosted.org/packages/4a/d8/c63d8adb6a7edaf8dcb6f75a2b1e9f8577960a1e489606859c4d73e7d32b/sqlalchemy-2.0.44-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9480c0740aabd8cb29c329b422fb65358049840b34aba0adf63162371d2a96e", size = 3214434, upload-time = "2025-10-10T15:47:00.473Z" }, - { url = "https://files.pythonhosted.org/packages/ee/a6/243d277a4b54fae74d4797957a7320a5c210c293487f931cbe036debb697/sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:17835885016b9e4d0135720160db3095dc78c583e7b902b6be799fb21035e749", size = 3155365, upload-time = "2025-10-10T15:34:17.932Z" }, - { url = "https://files.pythonhosted.org/packages/5f/f8/6a39516ddd75429fd4ee5a0d72e4c80639fab329b2467c75f363c2ed9751/sqlalchemy-2.0.44-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cbe4f85f50c656d753890f39468fcd8190c5f08282caf19219f684225bfd5fd2", size = 3178910, upload-time = "2025-10-10T15:47:02.346Z" }, - { url = "https://files.pythonhosted.org/packages/43/f0/118355d4ad3c39d9a2f5ee4c7304a9665b3571482777357fa9920cd7a6b4/sqlalchemy-2.0.44-cp310-cp310-win32.whl", hash = "sha256:2fcc4901a86ed81dc76703f3b93ff881e08761c63263c46991081fd7f034b165", size = 2105624, upload-time = "2025-10-10T15:38:15.552Z" }, - { url = "https://files.pythonhosted.org/packages/61/83/6ae5f9466f8aa5d0dcebfff8c9c33b98b27ce23292df3b990454b3d434fd/sqlalchemy-2.0.44-cp310-cp310-win_amd64.whl", hash = "sha256:9919e77403a483ab81e3423151e8ffc9dd992c20d2603bf17e4a8161111e55f5", size = 2129240, upload-time = "2025-10-10T15:38:17.175Z" }, - { url = "https://files.pythonhosted.org/packages/e3/81/15d7c161c9ddf0900b076b55345872ed04ff1ed6a0666e5e94ab44b0163c/sqlalchemy-2.0.44-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0fe3917059c7ab2ee3f35e77757062b1bea10a0b6ca633c58391e3f3c6c488dd", size = 2140517, upload-time = "2025-10-10T15:36:15.64Z" }, - { url = "https://files.pythonhosted.org/packages/d4/d5/4abd13b245c7d91bdf131d4916fd9e96a584dac74215f8b5bc945206a974/sqlalchemy-2.0.44-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:de4387a354ff230bc979b46b2207af841dc8bf29847b6c7dbe60af186d97aefa", size = 2130738, upload-time = "2025-10-10T15:36:16.91Z" }, - { url = "https://files.pythonhosted.org/packages/cb/3c/8418969879c26522019c1025171cefbb2a8586b6789ea13254ac602986c0/sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3678a0fb72c8a6a29422b2732fe423db3ce119c34421b5f9955873eb9b62c1e", size = 3304145, upload-time = "2025-10-10T15:34:19.569Z" }, - { url = "https://files.pythonhosted.org/packages/94/2d/fdb9246d9d32518bda5d90f4b65030b9bf403a935cfe4c36a474846517cb/sqlalchemy-2.0.44-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3cf6872a23601672d61a68f390e44703442639a12ee9dd5a88bbce52a695e46e", size = 3304511, upload-time = "2025-10-10T15:47:05.088Z" }, - { url = "https://files.pythonhosted.org/packages/7d/fb/40f2ad1da97d5c83f6c1269664678293d3fe28e90ad17a1093b735420549/sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:329aa42d1be9929603f406186630135be1e7a42569540577ba2c69952b7cf399", size = 3235161, upload-time = "2025-10-10T15:34:21.193Z" }, - { url = "https://files.pythonhosted.org/packages/95/cb/7cf4078b46752dca917d18cf31910d4eff6076e5b513c2d66100c4293d83/sqlalchemy-2.0.44-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:70e03833faca7166e6a9927fbee7c27e6ecde436774cd0b24bbcc96353bce06b", size = 3261426, upload-time = "2025-10-10T15:47:07.196Z" }, - { url = "https://files.pythonhosted.org/packages/f8/3b/55c09b285cb2d55bdfa711e778bdffdd0dc3ffa052b0af41f1c5d6e582fa/sqlalchemy-2.0.44-cp311-cp311-win32.whl", hash = "sha256:253e2f29843fb303eca6b2fc645aca91fa7aa0aa70b38b6950da92d44ff267f3", size = 2105392, upload-time = "2025-10-10T15:38:20.051Z" }, - { url = "https://files.pythonhosted.org/packages/c7/23/907193c2f4d680aedbfbdf7bf24c13925e3c7c292e813326c1b84a0b878e/sqlalchemy-2.0.44-cp311-cp311-win_amd64.whl", hash = "sha256:7a8694107eb4308a13b425ca8c0e67112f8134c846b6e1f722698708741215d5", size = 2130293, upload-time = "2025-10-10T15:38:21.601Z" }, - { url = "https://files.pythonhosted.org/packages/62/c4/59c7c9b068e6813c898b771204aad36683c96318ed12d4233e1b18762164/sqlalchemy-2.0.44-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72fea91746b5890f9e5e0997f16cbf3d53550580d76355ba2d998311b17b2250", size = 2139675, upload-time = "2025-10-10T16:03:31.064Z" }, - { url = "https://files.pythonhosted.org/packages/d6/ae/eeb0920537a6f9c5a3708e4a5fc55af25900216bdb4847ec29cfddf3bf3a/sqlalchemy-2.0.44-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:585c0c852a891450edbb1eaca8648408a3cc125f18cf433941fa6babcc359e29", size = 2127726, upload-time = "2025-10-10T16:03:35.934Z" }, - { url = "https://files.pythonhosted.org/packages/d8/d5/2ebbabe0379418eda8041c06b0b551f213576bfe4c2f09d77c06c07c8cc5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9b94843a102efa9ac68a7a30cd46df3ff1ed9c658100d30a725d10d9c60a2f44", size = 3327603, upload-time = "2025-10-10T15:35:28.322Z" }, - { url = "https://files.pythonhosted.org/packages/45/e5/5aa65852dadc24b7d8ae75b7efb8d19303ed6ac93482e60c44a585930ea5/sqlalchemy-2.0.44-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:119dc41e7a7defcefc57189cfa0e61b1bf9c228211aba432b53fb71ef367fda1", size = 3337842, upload-time = "2025-10-10T15:43:45.431Z" }, - { url = "https://files.pythonhosted.org/packages/41/92/648f1afd3f20b71e880ca797a960f638d39d243e233a7082c93093c22378/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0765e318ee9179b3718c4fd7ba35c434f4dd20332fbc6857a5e8df17719c24d7", size = 3264558, upload-time = "2025-10-10T15:35:29.93Z" }, - { url = "https://files.pythonhosted.org/packages/40/cf/e27d7ee61a10f74b17740918e23cbc5bc62011b48282170dc4c66da8ec0f/sqlalchemy-2.0.44-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2e7b5b079055e02d06a4308d0481658e4f06bc7ef211567edc8f7d5dce52018d", size = 3301570, upload-time = "2025-10-10T15:43:48.407Z" }, - { url = "https://files.pythonhosted.org/packages/3b/3d/3116a9a7b63e780fb402799b6da227435be878b6846b192f076d2f838654/sqlalchemy-2.0.44-cp312-cp312-win32.whl", hash = "sha256:846541e58b9a81cce7dee8329f352c318de25aa2f2bbe1e31587eb1f057448b4", size = 2103447, upload-time = "2025-10-10T15:03:21.678Z" }, - { url = "https://files.pythonhosted.org/packages/25/83/24690e9dfc241e6ab062df82cc0df7f4231c79ba98b273fa496fb3dd78ed/sqlalchemy-2.0.44-cp312-cp312-win_amd64.whl", hash = "sha256:7cbcb47fd66ab294703e1644f78971f6f2f1126424d2b300678f419aa73c7b6e", size = 2130912, upload-time = "2025-10-10T15:03:24.656Z" }, - { url = "https://files.pythonhosted.org/packages/45/d3/c67077a2249fdb455246e6853166360054c331db4613cda3e31ab1cadbef/sqlalchemy-2.0.44-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ff486e183d151e51b1d694c7aa1695747599bb00b9f5f604092b54b74c64a8e1", size = 2135479, upload-time = "2025-10-10T16:03:37.671Z" }, - { url = "https://files.pythonhosted.org/packages/2b/91/eabd0688330d6fd114f5f12c4f89b0d02929f525e6bf7ff80aa17ca802af/sqlalchemy-2.0.44-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0b1af8392eb27b372ddb783b317dea0f650241cea5bd29199b22235299ca2e45", size = 2123212, upload-time = "2025-10-10T16:03:41.755Z" }, - { url = "https://files.pythonhosted.org/packages/b0/bb/43e246cfe0e81c018076a16036d9b548c4cc649de241fa27d8d9ca6f85ab/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2b61188657e3a2b9ac4e8f04d6cf8e51046e28175f79464c67f2fd35bceb0976", size = 3255353, upload-time = "2025-10-10T15:35:31.221Z" }, - { url = "https://files.pythonhosted.org/packages/b9/96/c6105ed9a880abe346b64d3b6ddef269ddfcab04f7f3d90a0bf3c5a88e82/sqlalchemy-2.0.44-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b87e7b91a5d5973dda5f00cd61ef72ad75a1db73a386b62877d4875a8840959c", size = 3260222, upload-time = "2025-10-10T15:43:50.124Z" }, - { url = "https://files.pythonhosted.org/packages/44/16/1857e35a47155b5ad927272fee81ae49d398959cb749edca6eaa399b582f/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:15f3326f7f0b2bfe406ee562e17f43f36e16167af99c4c0df61db668de20002d", size = 3189614, upload-time = "2025-10-10T15:35:32.578Z" }, - { url = "https://files.pythonhosted.org/packages/88/ee/4afb39a8ee4fc786e2d716c20ab87b5b1fb33d4ac4129a1aaa574ae8a585/sqlalchemy-2.0.44-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e77faf6ff919aa8cd63f1c4e561cac1d9a454a191bb864d5dd5e545935e5a40", size = 3226248, upload-time = "2025-10-10T15:43:51.862Z" }, - { url = "https://files.pythonhosted.org/packages/32/d5/0e66097fc64fa266f29a7963296b40a80d6a997b7ac13806183700676f86/sqlalchemy-2.0.44-cp313-cp313-win32.whl", hash = "sha256:ee51625c2d51f8baadf2829fae817ad0b66b140573939dd69284d2ba3553ae73", size = 2101275, upload-time = "2025-10-10T15:03:26.096Z" }, - { url = "https://files.pythonhosted.org/packages/03/51/665617fe4f8c6450f42a6d8d69243f9420f5677395572c2fe9d21b493b7b/sqlalchemy-2.0.44-cp313-cp313-win_amd64.whl", hash = "sha256:c1c80faaee1a6c3428cecf40d16a2365bcf56c424c92c2b6f0f9ad204b899e9e", size = 2127901, upload-time = "2025-10-10T15:03:27.548Z" }, - { url = "https://files.pythonhosted.org/packages/9c/5e/6a29fa884d9fb7ddadf6b69490a9d45fded3b38541713010dad16b77d015/sqlalchemy-2.0.44-py3-none-any.whl", hash = "sha256:19de7ca1246fbef9f9d1bff8f1ab25641569df226364a0e40457dc5457c54b05", size = 1928718, upload-time = "2025-10-10T15:29:45.32Z" }, +sdist = { url = "https://files.pythonhosted.org/packages/ce/af/20290b55d469e873cba9d41c0206ab5461ff49d759989b3fe65010f9d265/sqlalchemy-1.4.54.tar.gz", hash = "sha256:4470fbed088c35dc20b78a39aaf4ae54fe81790c783b3264872a0224f437c31a", size = 8470350, upload-time = "2024-09-05T15:54:10.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/7f/f7c1e0b65790649bd573f201aa958263a389f336d6e000a569275ff9bd97/SQLAlchemy-1.4.54-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:af00236fe21c4d4f4c227b6ccc19b44c594160cc3ff28d104cdce85855369277", size = 1573472, upload-time = "2024-09-05T17:38:45.351Z" }, + { url = "https://files.pythonhosted.org/packages/e1/da/ff7f0fe50844496db523613979651f076f44da8625b8ad89c503dcff0a52/SQLAlchemy-1.4.54-cp310-cp310-manylinux1_x86_64.manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_5_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1183599e25fa38a1a322294b949da02b4f0da13dbc2688ef9dbe746df573f8a6", size = 1639088, upload-time = "2024-09-05T17:46:37.726Z" }, + { url = "https://files.pythonhosted.org/packages/04/45/3a35bb156aa2fd87b66a4992bb8d65593efd7e16ca2e0597e68c32c29037/SQLAlchemy-1.4.54-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1990d5a6a5dc358a0894c8ca02043fb9a5ad9538422001fb2826e91c50f1d539", size = 1627447, upload-time = "2024-09-05T17:45:32.379Z" }, + { url = "https://files.pythonhosted.org/packages/fe/5b/ed36a50e7147d0d090cd8e35de3b18d2c69a3e85df3be5fe42a570d6c331/SQLAlchemy-1.4.54-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:14b3f4783275339170984cadda66e3ec011cce87b405968dc8d51cf0f9997b0d", size = 1639081, upload-time = "2024-09-05T17:46:39.895Z" }, + { url = "https://files.pythonhosted.org/packages/4b/75/bfbdeb5dece7bc98acb414751a62ee43398b34b10133b1853f4282597757/SQLAlchemy-1.4.54-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b24364150738ce488333b3fb48bfa14c189a66de41cd632796fbcacb26b4585", size = 1638975, upload-time = "2024-09-05T17:46:41.569Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/358a9291d2fc3d51ad50557e126ad5f48200f199878437f7cb38817d607b/SQLAlchemy-1.4.54-cp310-cp310-win32.whl", hash = "sha256:a8a72259a1652f192c68377be7011eac3c463e9892ef2948828c7d58e4829988", size = 1591719, upload-time = "2024-09-05T17:52:26.646Z" }, + { url = "https://files.pythonhosted.org/packages/10/ad/87cd5578efdcef43a08ce4a21448192abf46bf69a5678ac0039e44364914/SQLAlchemy-1.4.54-cp310-cp310-win_amd64.whl", hash = "sha256:b67589f7955924865344e6eacfdcf70675e64f36800a576aa5e961f0008cde2a", size = 1593512, upload-time = "2024-09-05T17:51:21.402Z" }, + { url = "https://files.pythonhosted.org/packages/da/49/fb98983b5568e93696a25fd5bec1b789095b79a72d5f57c6effddaa81d0a/SQLAlchemy-1.4.54-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b05e0626ec1c391432eabb47a8abd3bf199fb74bfde7cc44a26d2b1b352c2c6e", size = 1589301, upload-time = "2024-09-05T19:22:42.197Z" }, + { url = "https://files.pythonhosted.org/packages/03/98/5a81430bbd646991346cb088a2bdc84d1bcd3dbe6b0cfc1aaa898370e5c7/SQLAlchemy-1.4.54-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:13e91d6892b5fcb94a36ba061fb7a1f03d0185ed9d8a77c84ba389e5bb05e936", size = 1629553, upload-time = "2024-09-05T17:49:18.846Z" }, + { url = "https://files.pythonhosted.org/packages/f1/17/14e35db2b0d6deaa27691d014addbb0dd6f7e044f7ee465446a3c0c71404/SQLAlchemy-1.4.54-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fb59a11689ff3c58e7652260127f9e34f7f45478a2f3ef831ab6db7bcd72108f", size = 1627640, upload-time = "2024-09-05T17:48:01.558Z" }, + { url = "https://files.pythonhosted.org/packages/98/62/335006a8f2c98f704f391e1a0cc01446d1b1b9c198f579f03599f55bd860/SQLAlchemy-1.4.54-cp311-cp311-win32.whl", hash = "sha256:1390ca2d301a2708fd4425c6d75528d22f26b8f5cbc9faba1ddca136671432bc", size = 1591723, upload-time = "2024-09-05T17:53:17.486Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/6b4b8c07082920f5445ec65c221fa33baab102aced5dcc2d87a15d3f8db4/SQLAlchemy-1.4.54-cp311-cp311-win_amd64.whl", hash = "sha256:2b37931eac4b837c45e2522066bda221ac6d80e78922fb77c75eb12e4dbcdee5", size = 1593511, upload-time = "2024-09-05T17:51:50.947Z" }, + { url = "https://files.pythonhosted.org/packages/a5/1b/aa9b99be95d1615f058b5827447c18505b7b3f1dfcbd6ce1b331c2107152/SQLAlchemy-1.4.54-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:3f01c2629a7d6b30d8afe0326b8c649b74825a0e1ebdcb01e8ffd1c920deb07d", size = 1589983, upload-time = "2024-09-05T17:39:02.132Z" }, + { url = "https://files.pythonhosted.org/packages/59/47/cb0fc64e5344f0a3d02216796c342525ab283f8f052d1c31a1d487d08aa0/SQLAlchemy-1.4.54-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9c24dd161c06992ed16c5e528a75878edbaeced5660c3db88c820f1f0d3fe1f4", size = 1630158, upload-time = "2024-09-05T17:50:13.255Z" }, + { url = "https://files.pythonhosted.org/packages/c0/8b/f45dd378f6c97e8ff9332ff3d03ecb0b8c491be5bb7a698783b5a2f358ec/SQLAlchemy-1.4.54-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5e0d47d619c739bdc636bbe007da4519fc953393304a5943e0b5aec96c9877c", size = 1629232, upload-time = "2024-09-05T17:48:15.514Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3c/884fe389f5bec86a310b81e79abaa1e26e5d78dc10a84d544a6822833e47/SQLAlchemy-1.4.54-cp312-cp312-win32.whl", hash = "sha256:12bc0141b245918b80d9d17eca94663dbd3f5266ac77a0be60750f36102bbb0f", size = 1592027, upload-time = "2024-09-05T17:54:02.253Z" }, + { url = "https://files.pythonhosted.org/packages/01/c3/c690d037be57efd3a69cde16a2ef1bd2a905dafe869434d33836de0983d0/SQLAlchemy-1.4.54-cp312-cp312-win_amd64.whl", hash = "sha256:f941aaf15f47f316123e1933f9ea91a6efda73a161a6ab6046d1cde37be62c88", size = 1593827, upload-time = "2024-09-05T17:52:07.454Z" }, ] [[package]] From 1fff2f0c1f40b9220f5afb759d62f2fb2785eaeb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pasternak?= Date: Sun, 19 Apr 2026 20:49:20 +0200 Subject: [PATCH 15/15] chore(deps): bump django-denorm-iplweb 1.10.1 -> 1.10.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Release 1.10.2 w forku user-a dodaje `get_joining_fields()` do inline'owej klasy `JoinField` w `TriggerFilterQuery.__init__` (`denorm/denorms.py:361`). Dzięki temu Django 6.0 przestaje emitować: RemovedInDjango60Warning: The usage of get_joining_columns() in Join is deprecated. Implement get_joining_fields() instead. Zweryfikowane przez uruchomienie `src/bpp/tests/test_views/test_oai.py` z `-W "error:The usage of get_joining_columns:DeprecationWarning"` — 6 passed, 0 warningów. --- pyproject.toml | 2 +- .../+denorm-bump-get-joining-fields.bugfix.rst | 6 ++++++ uv.lock | 8 ++++---- 3 files changed, 11 insertions(+), 5 deletions(-) create mode 100644 src/bpp/newsfragments/+denorm-bump-get-joining-fields.bugfix.rst diff --git a/pyproject.toml b/pyproject.toml index 617c977f3..cfccb3c9a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -11,7 +11,7 @@ dependencies = [ "arrow>=1.3,<2", "numpy>=2.1.2", "pygad>=3.6.0", - "django-denorm-iplweb>=1.10.1", + "django-denorm-iplweb>=1.10.2", "django-tabular-permissions==2.9.3", "simplejson>=3.19,<4", "django-reversion>=6,<7", diff --git a/src/bpp/newsfragments/+denorm-bump-get-joining-fields.bugfix.rst b/src/bpp/newsfragments/+denorm-bump-get-joining-fields.bugfix.rst new file mode 100644 index 000000000..039860523 --- /dev/null +++ b/src/bpp/newsfragments/+denorm-bump-get-joining-fields.bugfix.rst @@ -0,0 +1,6 @@ +Zbumpowano ``django-denorm-iplweb`` z ``>=1.10.1`` do ``>=1.10.2``. +Release 1.10.2 dodaje ``get_joining_fields()`` do inline'owej +klasy ``JoinField`` w ``TriggerFilterQuery`` (``denorm/ +denorms.py``), dzięki czemu Django 6.0 już nie emituje +``RemovedInDjango60Warning: The usage of get_joining_columns() in +Join is deprecated``. diff --git a/uv.lock b/uv.lock index 7b415ab89..e32394c08 100644 --- a/uv.lock +++ b/uv.lock @@ -429,7 +429,7 @@ requires-dist = [ { name = "django-crispy-forms", specifier = ">=2.4,<3" }, { name = "django-dbtemplates", specifier = ">=4.0" }, { name = "django-debug-toolbar", marker = "extra == 'dev'", specifier = ">=6.3.0" }, - { name = "django-denorm-iplweb", specifier = ">=1.10.1" }, + { name = "django-denorm-iplweb", specifier = ">=1.10.2" }, { name = "django-dirtyfields", specifier = "==1.9.9" }, { name = "django-dynamic-fixture", marker = "extra == 'dev'", specifier = ">=1.8.0" }, { name = "django-easy-audit", specifier = ">=1.3.9a2,<2" }, @@ -1681,7 +1681,7 @@ wheels = [ [[package]] name = "django-denorm-iplweb" -version = "1.10.1" +version = "1.10.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "celery", marker = "platform_python_implementation != 'PyPy'" }, @@ -1689,9 +1689,9 @@ dependencies = [ { name = "django", marker = "platform_python_implementation != 'PyPy'" }, { name = "tqdm", marker = "platform_python_implementation != 'PyPy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/78/d1f8326e46d9bebdf439d18714d6c24355ab5dc205fcf7cf7c0cf2af9297/django_denorm_iplweb-1.10.1.tar.gz", hash = "sha256:2da2aaa6adfe0be58fad1fbdbaeec85752841b60b4cfd2b83408c11b1a3b7ceb", size = 51979, upload-time = "2026-04-16T10:28:50.722Z" } +sdist = { url = "https://files.pythonhosted.org/packages/27/dc/0fc06068da69f49e441816076cfb3624cb992ee462c2b4253f29cee9f90c/django_denorm_iplweb-1.10.2.tar.gz", hash = "sha256:adf61edc4108e4d9c1fa866cc74176f8cf26a197e3270c7067888a46d1a2b8fa", size = 53737, upload-time = "2026-04-19T18:41:35.541Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a6/82/19c6a9bdad8618a58844c68a31cf7939555849cb990bc93ff5efa5ab68b6/django_denorm_iplweb-1.10.1-py3-none-any.whl", hash = "sha256:3155cde2b3ab3132f9dcc55dddd318bf2b2db5c93f863e1f68a3b319042a8187", size = 43209, upload-time = "2026-04-16T10:28:49.467Z" }, + { url = "https://files.pythonhosted.org/packages/92/3c/767728cb91747501733318fff8de0fa78839904ca5e26e021a9c8d616041/django_denorm_iplweb-1.10.2-py3-none-any.whl", hash = "sha256:71fffc8e5e198fe5484c59f806bb3f2eed804bc05ed9d9d466f7f8ab7a062535", size = 43595, upload-time = "2026-04-19T18:41:31.785Z" }, ] [[package]]