Feature/multi hosted config#189
Open
mpasternak wants to merge 243 commits into
Open
Conversation
Add infrastructure for multi-hosted BPP configuration where one server can serve multiple universities on different domains. - Uczelnia: add OneToOne to django.contrib.sites.Site + theme_name field - BppUser: add M2M accessible_sites for per-user university access control - SiteResolutionMiddleware: resolves hostname → Site → Uczelnia on every request - Context processors: site-aware cache keys, per-uczelnia theme selection - Admin: site/theme fields in UczelniaAdmin, accessible_sites in BppUserAdmin - Data migration: links existing Uczelnia to Site(pk=1), grants staff access Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Move all per-uczelnia settings from django-constance to fields on the Uczelnia model, enabling per-university configuration in multi-hosted mode. - Add 8 new fields to Uczelnia: google_analytics_property_id, google_verification_code, pokazuj_oswiadczenie_ken, skrot_wydzialu_w_nazwie_jednostki, wydruk_margines_* (4 fields) - Context processor reads from Uczelnia instead of constance.config - Admin mixin reads scoring settings from Uczelnia instead of constance - Empty CONSTANCE_CONFIG (all settings migrated to Uczelnia) - Data migration copies existing Constance values to Uczelnia - UczelniaAdmin: add new fieldsets for Google, structure, margins Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add SiteFilteredAdminMixin to filter admin querysets by current uczelnia. Regular admins see only their university's data, superusers see all. - New SiteFilteredAdminMixin in src/bpp/admin/helpers/site_filtered.py - Applied to JednostkaAdmin (uczelnia), WydzialAdmin (uczelnia), AutorAdmin (aktualna_jednostka__uczelnia), UczelniaAdmin (pk filter) - FK dropdowns for wydzial/jednostka filtered per-uczelnia - Middleware blocks admin access for staff with accessible_sites configured but missing current site (backward compat: no sites = allow) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add uczelnia ForeignKey to PBN models that store per-institution data, enabling multi-tenant PBN operations. - Add uczelnia FK to: OsobaZInstytucji, PublikacjaInstytucji, PublikacjaInstytucji_V2, OswiadczenieInstytucji, SentData - Data migration links all existing records to first Uczelnia - Apply SiteFilteredAdminMixin to all 5 PBN admin classes - Fix PublikacjaInstytucji_V2.link_do_pi() to use self.uczelnia Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Add site_cache_key utility and update admin filter count cache to include site_id, preventing cross-tenant cache pollution. - New src/bpp/cache_utils.py with site_cache_key() utility - Admin filter_count_view cache key includes site.pk - Fix test cache invalidation for per-site keys Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Uczelnia.objects.get_default() with proper multi-site patterns: - Views: use get_for_request(request) or self.request - Celery tasks: add uczelnia_id parameter with fallback to get_default() - Management commands: add --uczelnia-id argument - Refactor pbn_integrator handle() to reduce complexity (C901 33→<10) - Fix UP031 percent format → f-strings in ranking_autorow - Refactor ranking get_queryset() to reduce complexity (C901 13→<10) 28 files updated across tasks, commands, views, admin helpers, and menu. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Replace Uczelnia.objects.get_default() in model/utility code that has no request context, by adding uczelnia parameter with fallback. bpp models: - jednostka.py: use self.uczelnia in get_default_ordering - abstract/pbn.py: add uczelnia param to link_do_pbn, _format_link_pi - abstract/disciplines.py: add uczelnia param to przelicz_punkty_dyscyplin - multiseek_registry/fields: add uczelnia param to option_enabled - admin/helpers/pbn_api/cli.py: add uczelnia param, fix B904 raise from PBN import/integrator utilities: - pbn_import/utils: add uczelnia param to all importer classes - pbn_integrator/utils: add uczelnia param to scientists and institutions - pbn_import/templatetags: use request from template context Other: - zglos_publikacje: forms accept uczelnia kwarg, model uses _uczelnia attr - importer_publikacji: add uczelnia param to helpers Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- SITE_ID configurable via DJANGO_BPP_SITE_ID env var (default=1) - Add DJANGO_BPP_ENABLE_SITEMAPS env var to disable static-sitemaps in multi-hosted mode (static-sitemaps generates for one domain only) - django_countdown already multi-site friendly (uses get_current_site) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
New test fixtures (conftest_multisite.py): - site1/site2, uczelnia1/uczelnia2, staff users per-site - wydzial/jednostka/autor per-uczelnia - make_request_for_site() helper for simulating domain requests Middleware tests (test_site_resolution.py, 9 tests): - Hostname→Site→Uczelnia resolution - Fallback to SITE_ID for unknown hosts - Staff blocked from wrong site's admin (403) - Superuser allowed everywhere - Anonymous allowed on public pages - Backward compat: staff with no sites configured Admin filtering tests (test_site_filtered.py, 5 tests): - Jednostka/Wydzial filtered per-uczelnia for staff - UczelniaAdmin shows only own uczelnia for non-superuser - Superuser sees all data Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Article model: add M2M uczelnie field (default: all universities) - ArticleAdmin: filter_horizontal for uczelnie, auto-assign all on create - Browse view: filter articles, recently_updated, abstracts, total count by authors from current uczelnia's units - Root view: use get_for_request instead of .first() - Data migration: assign existing articles to all uczelnie - Fix get_absolute_url to use self.uczelnie.first() Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- PBN_Export_Queue: add uczelnia FK + SiteFilteredAdminMixin in admin Data migration links existing records to first Uczelnia - Deduplikator autorów/publikacji: has_module_permission = superuser only (deduplikacja jest operacją globalną, nie per-uczelnia) - Rozbieżności IF/PK/dyscyplin: TODO markers for per-uczelnia filtering Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Rozbieżności dyscyplin: - RozbieznosciViewAdmin/RozbieznosciZrodelViewAdmin: filter by autor__aktualna_jednostka__uczelnia for non-superusers Rozbieżności IF/PK: - RozbieznosciIfLogAdmin/RozbieznosciPkLogAdmin: filter by rekord__autorzy_set__jednostka__uczelnia with distinct() - IgnorujRozbieznoscIf/PkAdmin: superuser-only (GenericFK) Autocomplete: - AutorAutocompleteBase: filter by aktualna_jednostka__uczelnia when request._uczelnia is set (admin context) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
5 integration tests verifying multi-site data isolation: - Article visible only on assigned uczelnia - Article on both uczelnie when both assigned - Staff cannot see other uczelnia's jednostki in admin - Staff gets 403 on wrong uczelnia's admin - Browse record count scoped per uczelnia Fix: browse view queryset used invalid `original__autorzy_set` path (original is a cached_property, not a DB field). Changed to `autorzy__jednostka__in` which is the correct ORM path for Rekord materialized view. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Per Ultraplan review: change BppUser M2M from Site to Uczelnia for clearer semantics — user has access to universities, not domains. - BppUser.accessible_sites (M2M→Site) → accessible_uczelnie (M2M→Uczelnia) - Migration: add new field, copy data (Site→Uczelnia via OneToOne), remove old - Middleware: check access by uczelnia instead of site - Admin: update fieldset reference - Fixtures + tests: updated to use accessible_uczelnie Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Łączy leafy grafu migracji powstałe po rebase feature/multi-hosted-config: - bpp: 0413_bppuser_autor_onetoone (dev) + 0415_rename_accessible_sites_to_uczelnie (feature) - miniblog: 0003_alter_article_article_body (dev) + 0004_assign_articles_to_all_uczelnie (feature)
Dwie pozostałości po Phase 4 — Uczelnia.objects.first() i .all().first() w widoku rankingu autorów. W multi-site zwracały losową uczelnię zamiast tej z bieżącego requestu, przez co podgląd "uzywaj_wydzialow" i "pokazuj_liczbe_cytowan_w_rankingu" nie respektował ustawień uczelni hostującej daną stronę.
Code: - autocomplete/authors.py: getattr(getattr(self, "request", None), ...) zamiast self.request — view'y są instancjonowane bezpośrednio w testach bez routingu HTTP. - browse.py:JednostkiView.get_paginate_by: użyj None-safe Uczelnia.objects.get_for_request, zamiast hasattr-ochrony zwracającej static fallback. Testy zaktualizowane do nowego API: - test_handlers.test_handler403_permission_denied: @pytest.mark.django_db (SiteResolutionMiddleware sięga do DB jak handler403/404/500). - pbn_export_queue test_admin: patch admin.ModelAdmin.response_change zamiast __bases__[1] (po dodaniu SiteFilteredAdminMixin baza ModelAdmin przesunęła się na index 2). - test_browse: a.uczelnie.set([uczelnia]) — Article jest M2M-przypisany do uczelni od Phase: Miniblog M2M. - test_oai, test_ewaluacja_no_queries: bump query budgetu o +3 (Site.get + site.uczelnia + cache lookup z SiteResolutionMiddleware). ImportError w django_pg_baseline/tests/test_rebuild.py jest pre-existing na dev (eb1a124), nie regresja tej gałęzi.
Bugfixy (request był dostępny, ale używano get_default()/first()): - bpp/context_processors/orcid.py — orcid_login_enabled flag. - orcid_integration/backends.py — auth backend's authenticate(request) ignorował request. Realny problem bezpieczeństwa: w multi-site uczelnia.orcid_tylko_dla_pracownikow rozstrzygane było po losowej uczelni, nie tej z hosta. - bpp/admin/jednostka.py — get_changeform_initial_data(self, request). - ranking_autorow: refactor RankingAutorowForm — sygnatura __init__(self, lata, *args, request=None, **kwargs), klasowa lambda w polu rozbij_na_jednostki przeniesiona do __init__. View przekazuje request przez get_form_kwargs. Site OneToOne obowiązkowe: - Model: usunięto null=True, blank=True z Uczelnia.site. - Migracja 0417_ensure_uczelnia_site_not_null: data migration fail-loudly dla niejednoznacznych przypadków, AlterField NOT NULL. - Setup wizard (UczelniaSetupForm.save) — auto-link do get_current_site(request). - Admin (UczelniaAdmin.save_model) — auto-link przy tworzeniu nowej Uczelni. - Test util any_uczelnia + fixture uczelnia w conftest_models — get_or_create Site(domain="testserver") jeśli nie podano. - test_views_browse: zamiana Uczelnia.objects.create(...) na any_uczelnia(...). Pełny suite: 3682 passed, 0 failed.
Zmiana semantyki przypisania artykułu do uczelni: - Niepusty M2M ``Article.uczelnie`` = artykuł widoczny tylko na wybranych uczelniach (bez zmian). - Pusty M2M = artykuł widoczny na WSZYSTKICH uczelniach (lazy resolution zamiast eager-assignment z ArticleAdmin.save_model). Zalety vs. poprzednia implementacja (admin save_model assign all): - Nowo utworzona Uczelnia automatycznie widzi artykuły z pustym M2M (przed zmianą trzeba było ręcznie edytować artykuły dodane przed nową uczelnią). - Edycja artykułu z czyszczeniem M2M = "pokazuj wszędzie" (przed: artykuł znikał wszędzie, bo save_model sprawdzał `not change`). Implementacja: - ``Article.objects.visible_on(uczelnia)`` manager method z ``Q(uczelnie=uczelnia) | Q(uczelnie__isnull=True)``. - ``bpp.views.browse.get_uczelnia_context_data`` używa ``visible_on`` zarówno dla listy ostatnich artykułów, jak i dla pojedynczego artykułu (``get_object_or_404``). - Usunięto ``ArticleAdmin.save_model`` (eager-assignment do wszystkich). Tests: - ``test_article_with_empty_m2m_visible_on_all_uczelnie`` — nowy test weryfikujący lazy resolution. - Istniejące testy isolation/explicit-assignment zostają zielone. Brak migracji — zgodnie z decyzją, brak istniejących instalacji do zaktualizowania.
Plik .docker-build juz nie istnieje (skasowany w poprzednim commicie), wiec elif sprawdzajacy `[ -f ".docker-build" ]` byl dormantnym kodem. Zastapione: push na non-master (czyli feature/fix/hotfix przez restrykcje triggera) → buduj zawsze. Realizuje user-intent "auto-build na feature branches" — bez tego push na feature spadalby na else (skip), a `.docker-build` flag nie istnieje. Komentarze i opisy aktualizowane — bez wzmianek o pliku flagi. Pozostale `docker-build` w workflow to label PR-a (mechanizm zostaje). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wycofuje gating labelem .docker-build/docker-build na rzecz prostszej zasady: master/main push i workflow_dispatch buduja zawsze (release flow + manual override), pozostale (PR sync, feature/fix/hotfix push bez PR) — tylko gdy actor=mpasternak. Inni contributorzy nie pala Docker Cloud minutek; jesli trzeba zbudowac obraz dla cudzego PR-a: `gh workflow run build-docker-images.yml --ref <branch>`. Dev branch dopisany jawnie do komentarza w pushu jako "intentionally excluded" — push do dev nie odpala buildu (intermediate state nie zasluguje na obraz, release leci przez master). Dodany main do triggerow obok master (gdyby kiedys repo zmienilo default branch — single source of truth). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Multi-hosted deployment (jedna instalacja BPP, wiele uczelni/domen)
nie mieścił się w pojedynczym DJANGO_BPP_HOSTNAME. Wprowadzona zmienna
DJANGO_BPP_HOSTNAMES (CSV) rozwiązuje to bez breaking change:
- Jeśli ustawisz DJANGO_BPP_HOSTNAMES, jest source-of-truth dla
ALLOWED_HOSTS i CSRF_TRUSTED_ORIGINS. Pierwszy element listy staje się
canonical hostname (settings.DJANGO_BPP_HOSTNAME) — wykorzystywany przez
Rollbar do identyfikacji deployment'u w raportach błędów.
- Jeśli HOSTNAMES jest puste, używamy single DJANGO_BPP_HOSTNAME jak
wcześniej. Existujące deployments nie wymagają zmian konfiguracji.
Zmienione pliki:
- settings/base.py: parsing CSV w DJANGO_BPP_HOSTNAMES, derive HOSTNAME
z pierwszego elementu listy.
- settings/local.py, production.py: ALLOWED_HOSTS rozszerzony o pełną
listę zamiast pojedynczego env('DJANGO_BPP_HOSTNAME').
- .env.docker, .env.example: udokumentowano obie zmienne i ich relację.
Tests: 3683 passed, 0 failed (full suite).
Walidacja konfiguracji (base.py): - DJANGO_BPP_HOSTNAME i DJANGO_BPP_HOSTNAMES ustawione naraz → ImproperlyConfigured (intencja niejasna). - DJANGO_BPP_HOSTNAME zawiera przecinek → ImproperlyConfigured (na multi-host używaj HOSTNAMES). - DJANGO_BPP_HOSTNAMES bez przecinka lub tylko jeden host po sparsowaniu → ImproperlyConfigured (na single-host używaj HOSTNAME). Custom Rollbar middleware (bpp/middleware.py): - Dotychczasowy DJANGO_BPP_HOSTNAME (canonical/installation identity) zostaje. - Dodatkowo per-request: request_host (vhost gdzie padło zgłoszenie) + uczelnia_skrot/uczelnia_pk z request._uczelnia (ustawiane przez SiteResolutionMiddleware). - DisallowedHost przy request.get_host() łapany ostrożnie i raportowany jako sentinel "<DisallowedHost>" — Rollbar handler nie powinien failować przy raportowaniu błędu, który sam jest DisallowedHost. Tests: 3683 passed, 0 failed.
Pięć miejsc używało Site.objects.first()/get_current() do budowy URL-i
w eksportach XLSX/BibTeX. W multi-hosted to losowy host — eksport
wygenerowany na uczelnia1 mógł zawierać linki na uczelnia2.
Wspólny helper bpp.util.site_url_for_request(request=None):
- z requestem: f"{scheme}://{host}".
- bez requestu (CLI/Celery): fallback do Uczelnia.objects.get_default()
.site, dalej Site.objects.first(), ostatecznie "https://localhost".
Naprawione miejsca:
- bpp/admin/xlsx_export/resources.py: Wydawnictwo_ResourceBase trzyma
request z kwargs (przekazane przez ImportExportModelAdmin).
- rozbieznosci_dyscyplin/admin.py: RozbieznosciViewResource +
RozbieznosciZrodelViewResource analogicznie.
- deduplikator_autorow/utils/export.py + views.py: export_duplicates_to_xlsx
bierze request opcjonalnie, propagacja z download_duplicates_xlsx.
- deduplikator_zrodel/utils.py + views.py: analogicznie.
- ewaluacja2021/util.py: output_table_to_xlsx (CLI/Celery context),
helper fallbackuje do default Uczelnia.site.
Drobne pre-existing fixy w ewaluacja2021/util.py (wymagane przez
ruff hook): rename `a`/`col`/`dirs` na `_`, # noqa: E402 dla
intencjonalnych mid-file imports, # noqa: C901 dla output_table_to_xlsx.
Plus IDE fix w bpp/admin/uczelnia.py:save_model: try/except
ImproperlyConfigured przy obj.pbn_client() (gdy admin ustawi
pbn_integracja=True ale nie wypełni pbn_app_name/token).
Tests: 3683 passed, 0 failed.
Dodaje przycisk „Importuj" w admin/bpp/jednostka/. Plik XLSX (kolumny:
Uczelnia, Wydział, Katedra/Zakład/Klinika) jest parsowany przez nowy
JednostkaImportResource:
- Uczelnie muszą istnieć (lookup po nazwa) — błąd per-wiersz w GUI.
- Brakujące Wydziały tworzone get_or_create przez WydzialGetOrCreateWidget
z auto-generowanym skrot (max 10) i skrot_nazwy (max 250).
- Puste komórki Wydział/Katedra dostają domyślne nazwy
(„Wydział <skrót uczelni>", „Jednostka Wydziału <X>").
- import_id_fields=("nazwa",) + skip_unchanged → idempotentny re-import.
- before_save_instance auto-generuje Jednostka.skrot i ustawia
aktualna=True na nowych wierszach.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wcześniej autocomplete twardo filtrował autorów po aktualna_jednostka.uczelnia == request._uczelnia, przez co nie dało się wybrać: - wieloetatowca z aktualną jednostką w innej uczelni federacji - byłego pracownika (brak aktualnej jednostki, ale Autor_Jednostka u nas) - autora bez żadnego przypisania (np. świeżo zaimportowanego z PBN) Zamiast filtrować, autocomplete annotuje każdy wynik etykietą grupy (Case/When + Exists na Autor_Jednostka) i sortuje po niej. Override get_results renderuje 3 optgroupy w odpowiedzi Select2 — JS po stronie klienta nie wymaga zmian (Select2 obsługuje optgroup natywnie): ✅ Autorzy z naszej uczelni 🏛️ Autorzy powiązani historycznie z naszą uczelnią 🌐 Autorzy zewnętrzni get_result_label zostaje bez zmian — emoji per-option (📚 PBN, 🏛️ MNISW, [❌ USUNIĘTY]) działa jak wcześniej. Naprawia 5 testów Playwright padających pre-merge na multi-hosted-config: test_podpowiedzi_dyscyplin_autor_ma_jedna_uczelnia_podpowiada (ciagle/zwarte) oraz test_procent_odpowiedzialnosci AutorFormset jeden_autor (ciagle/zwarte) i dobrze_potem_zle_dwoch_autorow (patent). Wszystkie 5 używały autorów bez aktualna_jednostka, których stary filtr odsiewał z autocomplete.
…tury journal_id Po merge dev: _process_journal_thread_safe dostaje journal_id (nie obiekt Journal) i ładuje Journal w wątku — optymalizacja szczytu pamięci z dev (#329). Test catch-all/Rollbar z brancha multi-hosted-config wołał starą sygnaturę (pbn_journal). Aktualizacja: przekazujemy journal_id i pacujemy Journal.objects.get; intencja (logger.exception + rollbar + status błędu bez rzucania) bez zmian. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…hanged files) Cztery pliki dotknięte przez branch były zacommitowane bez ruff-format i CI „Lint changed files" (pre-commit ruff-format na diffie vs origin/dev) je łapał. Tylko formatowanie — zero zmian logiki. Odtworzone 1:1 komendą CI (uvx pre-commit run ruff-format --from-ref origin/dev --to-ref HEAD): 4 files reformatted, 269 left unchanged. Migracje pominięte (exclude hooka). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ta/rok per uczelnia test_lata/rok_..._per_uczelnia (dodane w 75de645, R3a) wołają dodaj_autora, które domyślnie szuka Typ_Odpowiedzialnosci(skrot="aut."). Testy nie żądały fixture'a typy_odpowiedzialnosci, więc padały na DoesNotExist w setupie — pre-existing regres brancha, maskowany wcześniej przez awarie get_default w tych samych shardach. Fix jak w sąsiednich testach per-uczelnia (test_lata_autocomplete_per_uczelnia.py). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Dev przywrócił UczelniaManager.get_default/_get_default + cached_property default z DeprecationWarning (8c6eb3e). W tej gałęzi „uczelnia domyślna" NIE ISTNIEJE — guard test test_get_default_usuniete_na_trwale zabrania get_default ORAZ .default. Konflikt w src/bpp/models/uczelnia.py rozwiązany na rzecz multi-hosted: - odrzucono dev-owe get_default/_get_default/default, - zachowano get_single_uczelnia_or_none/_or_fail + get_for_pbn_background, - usunięto osierocony import warnings. Skan runtime src/: 0 wołań Uczelnia.objects.get_default()/.default. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…ring sanitization' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
GET /__external_auth/login/ (redirect z Grafany/Dozzle dla niezalogowanych)
zwracał 500. Django przy inicjalizacji silnika szablonów ładuje ZACHŁANNIE
wszystkie moduły templatetags/*.py każdej apki z INSTALLED_APPS — więc
bpp.templatetags.bpp_formdefaults był importowany, a jego top-levelowy
`from formdefaults.models import FormRepresentation` wywalał się na
RuntimeError ("Model class ... doesn't declare an explicit app_label"),
bo formdefaults nie było w INSTALLED_APPS lekkiego authservera.
Fix: dodaj "formdefaults" do INSTALLED_APPS w auth_server.py.
Regresja: dwa testy w test_auth_server.py — jeden pilnuje obecności
formdefaults w INSTALLED_APPS, drugi renderuje auth_server/login.html
(dokładnie ścieżka, która padała na produkcji).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…osted
Import publikacji startujący od późniejszego kroku (np. od źródeł, z
pominięciem kroku institution_setup) wywalał się na:
ValueError: Nie znaleziono domyślnej jednostki dla importu publikacji
Przyczyna — niespójność kluczy w ImportSession.config: formularz nowego
importu zapisywał wybór jako config["jednostka_domyslna_id"], a kroki
publication_import / statement_import czytały WYŁĄCZNIE
config["default_jednostka_id"] — klucz zapisywany tylko przez krok
institution_setup. Bez tego kroku klucz nie istniał, a ślepy fallback
filter(nazwa="Jednostka Domyślna") (dokładne dopasowanie nazwy, bez filtra
uczelni) zwracał None → wyjątek.
Zmiany:
1. Ujednolicenie wyznaczania domyślnej jednostki — nowy resolver
resolve_default_jednostka(session, uczelnia): default_jednostka_id →
jednostka_domyslna_id → uczelnia-aware znajdz_lub_utworz_jednostke_domyslna.
Używany przez publication_import; statement_import reużywa wyniku
(miał ten sam bug przez drugi odczyt config).
2. Formularz (StartImportView.post) zapisuje też kanoniczne klucze
default_jednostka_id / wydzial_id już na etapie startu, więc każdy krok
zna wybór niezależnie od tego, który krok ruszy pierwszy.
3. Gate-check multi-hosted: domyślna jednostka i wydział MUSZĄ należeć do
uczelni z requestu. GET zawęża listy jednostek/wydziałów do tej uczelni;
POST odrzuca start importu z encją obcej uczelni (ochrona przed cichym
przypisaniem prac/autorów do tenanta B z kontekstu tenanta A).
Testy: nowy test_default_jednostka_resolution.py (resolver) + testy gate-check,
zapisu kanonicznego klucza i filtrowania listy w GET w test_views_dashboard.py.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Domknięcie luki, przez którą bug wszedł na produkcję: formularz (producent) i kroki importu (konsument) były testowane OSOBNO — nikt nie przepuścił configu zapisanego przez formularz przez PublicationImporter._setup_uczelnia_and_jednostka (metoda rzucająca ValueError miała 0 testów). Nowy test end-to-end reprodukuje dokładnie produkcyjny traceback: sesja z configiem zawierającym WYŁĄCZNIE jednostka_domyslna_id (institution_setup pominięty, import od źródeł) → _setup musi rozwiązać jednostkę bez wyjątku. Zweryfikowane red→green: przeciw staremu kodzie test pada ValueError "Nie znaleziono domyślnej jednostki" (publication_import.py), z poprawką przechodzi. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Test mierzył wybór plannera zależny od statystyk — przechodził w izolacji (świeża, nie-ANALYZE-owana tabela → heurystyka ~10 stron → Index Scan), padał w pełnej suite (autoanalyze ustawiał relpages=1 → Seq Scan na 1-stronicowej tabeli). Teraz test zasiewa ~600 wierszy (klon jednym INSERT...SELECT × generate_series) + ANALYZE, więc planner naturalnie wybiera Index Scan po PK; order-independent. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nto feature/multi-hosted-config
ImportManager — plik spinający wszystkie kroki importu — miał ~21% pokrycia, mimo że to on decyduje o autoryzacji, pomijaniu kroków, obsłudze błędów i anulowaniu. To najgęstsza logika decyzyjna w pbn_import, a była praktycznie nietestowana (najbardziej prawdopodobne miejsce następnego buga typu "contract drift"). Nowy test_import_manager.py (18 testów): - _check_pbn_authorization: brak klienta / klient OK / klient rzuca, - _extract_error_message: JSON description → message → raw → str(exc), - _validate_pbn_authorization: rzuca gdy krok wymaga PBN bez autoryzacji, przechodzi dla samych kroków konfiguracyjnych, - _should_skip_step: tabela (setup vs nie-setup × auth), - _has_error_logs, - run() end-to-end na atrapach kroków: happy path → completed, anulowanie w trakcie startu → cancelled (krok nie biegnie), błąd kroku required → failed, - pause/resume/cancel ustawiają status. Pokrycie pbn_import jako całość: 33.7% → 38.5%. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Po zakończonym imporcie (status=completed) strona szczegółów sesji dostaje
nową zakładkę „Log": podgląd symulowanego raw logu w <pre> + przycisk
„Pobierz log (.txt)".
Raw logu nie mamy fizycznie — odtwarzamy go z wpisów ImportLog. Zawartość to
te same dane co zakładka „Błędy i ostrzeżenia" (poziomy error/critical/
warning): czas, klasa błędu (details.exception), moduł (step), pełna
wiadomość, kontekst oraz — gdy są — pełne tracebacki. Wpisy chronologicznie
(najstarsze u góry), jak w prawdziwym pliku logu.
Komponenty:
- utils/log_export.py: render_session_log_text(session) -> str (czysta,
testowalna funkcja, bez HTTP),
- ImportLogDownloadView (GET session/<pk>/log.txt/): text/plain + attachment,
dostępne tylko dla completed (inaczej 404), tylko właściciel/superuser,
- ImportSessionDetailView: raw_log_text w kontekście dla completed,
- session_detail.html: zakładka + panel #log-panel pod {% if completed %}.
Bez zmian modelu/migracji — log jest pochodną istniejącego ImportLog.
Testy: test_log_export.py (format, filtr poziomów, kolejność, traceback,
brak błędów, brak details) + testy widoku pobierania i widoczności zakładki.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Korekta po feedbacku: zakładka „Log" + pobieranie były zawężone do status=completed, więc na nieudanym imporcie (failed) — czyli tam, gdzie log jest najpotrzebniejszy — nie było ich widać. Teraz zakładka i pobranie działają dla KAŻDEGO statusu (completed/failed/ cancelled/running/pending). Dla biegnącego importu to migawka z chwili renderu/żądania. Usunięto gate completed-only w widoku szczegółów, w ImportLogDownloadView (już bez Http404) i w szablonie. Testy zaktualizowane: zamiast „running ukrywa zakładkę" — parametryzowane testy, że zakładka jest dla każdego statusu, a pobranie działa też dla failed/cancelled/running/pending. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ugim) Wyświetlanie: zakładka „Log" wrzucała CAŁY log do <pre> przy każdym wejściu w szczegóły sesji. Przy tysiącach błędów z tracebackami to MB HTML-a w jednym <pre> → zatkana przeglądarka + spuchnięty payload strony (a te same dane są już w zakładce „Błędy i ostrzeżenia"). Podgląd inline ograniczony do PREVIEW_LIMIT (100) pierwszych wpisów + baner „podgląd przycięty: pokazano X z Y — pobierz pełny log". Pełny log bez zmian przez pobranie .txt (render_session_log_text bez limitu) — zgodnie z uwagą, że pobieranie jest OK, nie ruszam go (streaming można dodać później). - log_export.render_session_log_text(session, limit=None): slice qs[:limit] + stopka o przycięciu; count_log_entries() do flagi truncated, - widok szczegółów: raw_log_text (limit=PREVIEW_LIMIT) + raw_log_total/ raw_log_shown/raw_log_truncated, - szablon: baner callout gdy przycięte. Testy: limit przycina i odnotowuje, brak stopki bez limitu/w granicy, count liczy tylko error+warning; widok ustawia flagi i pokazuje baner (patch limitu). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…długich tagów (#331) Import publikacji z PBN wywalał się na dwóch klasach danych: 1. Słowa kluczowe (taggit Tag.name/slug = varchar(100)) — PBN bywa sklejony w jeden bardzo długi ciąg bez separatorów, przekraczał limit i wywracał import na DataError. Teraz: za długie tagi są pomijane, logowane i zapisywane do `adnotacje` pod znacznikiem `tagsTooLong` (do ręcznej korekty), a rekord się importuje. 2. Tytuły w językach innych niż eng/pol — importer brał tylko eng/pol do `tytul` i asertował pusty słownik `titles`, więc deu/rus/lit wywalały AssertionError, a brak eng/pol — KeyError. Teraz tytuły w pozostałych językach trafiają do nowych wierszy `Wydawnictwo_{Ciagle,Zwarte}_Tytul` (analogicznie do streszczeń), z dowiązaniem do słownika `Jezyk` jeśli się da (`pbn_uid_id` → fallback `skrot`), a surowy kod PBN zachowany w `kod_jezyka_pbn` nawet gdy języka nie ma w słowniku. `tytul` nadal dostaje eng→pol (bez regresji wyświetlania/wyszukiwania). - nowy abstrakt BazaModeluTytulow + 2 modele konkretne + migracja 0430 - inline w adminie obu wydawnictw (edytowalne ręcznie) - przetworz_tytuly(pbn_json, ret, klasa_tytulu) woła się po ret.save() - testy: tagi (pomijanie/adnotacje) + tytuły (eng/pol, obce języki, brak eng/pol, dowiązanie Jezyk, nieznany język, unikalność) Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…334) * docs(oidc): spec naprawy logowania OIDC + menu instytucjonalne Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(oidc): napraw logowanie OIDC (claimy mail->email, easyaudit) + menu instytucjonalne per-uczelnia Naprawa logowania OIDC (Keycloak, realm KA): * Claimy: realm wystawia adres pod kluczem \`mail\`, a mozilla-django-oidc oczekuje \`email\` (verify_claims/filter_users_by_claims/create_user) - normalizujemy w jednym chokepoincie get_userinfo() (mail->email). Username z preferred_username, imie/nazwisko z given/family_name. * KeyError: 'username' z easyaudit: jego handler user_login_failed robi twardo credentials[USERNAME_FIELD], a callback OIDC wola authenticate() bez username -> 500 przy PROPAGATE_EXCEPTIONS=True. Guard (apps.ready, gdy easyaudit zainstalowany) podmienia receiver na odporny wariant, delegujacy do oryginalnej logiki easyaudit. * Banner [OIDC] z claimami: stderr -> logger.debug. Menu "logowanie instytucjonalne" jak Microsoft, ale PER-UCZELNIA: * oidc_integration/access.py: oidc_enabled_for_request() - wspolne zrodlo prawdy dla menu i routingu. OIDC to jeden realm na proces, wiec gateujemy po skrocie uczelni (request._uczelnia.skrot == OIDC_LOGIN_SKROT); bez skrotu = instalacja jedno-uczelniana -> globalnie. Precedencja: OIDC (per-uczelnia) > Microsoft (globalny) > formularz BPP. * InstitutionalLoginView jako login_form; local_login_form = formularz BPP. * top_bar.html: instytucjonalne (OIDC/Microsoft) + logowanie BPP w menu. * registration/login.html: usuniety przycisk OIDC spod formularza. Testy: backendy (normalizacja, debug log), easyaudit guard, per-uczelnia gating, dyspozytor logowania. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…microsoft_auth W trybie mieszanym (microsoft_auth + oidc_integration) /logout/ obsługuje MicrosoftLogoutView dla wszystkich backendów. Nie był on świadomy OIDC, więc sesja zalogowana przez Keycloaka szła na logout Microsoftu i zostawała żywa sesja SSO w Keycloaku (cicha re-autoryzacja przy kolejnym logowaniu). MicrosoftLogoutView rozpoznaje teraz sesję OIDC po BACKEND_SESSION_KEY i kieruje ją na RP-Initiated Logout Keycloaka (URL budowany przed logout(), bo czyta oidc_id_token z sesji). Sesje Microsoft/lokalne/ORCID bez zmian. Dodatkowo: jawny TODO „GATE PRZED PRODUKCJĄ" w BppOIDCBackend — provisioning fazy 2a (konto każdemu userowi realmu) nie może wejść na produkcję bez gate'u. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…ring sanitization' Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Naprawia 4 klasy błędów wywalających import PBN (z logu produkcyjnego): - AssertionError na rozdziałach: sub-słownik chapters niesie redundantne `year`/`originalLanguage`, których nic nie konsumowało — popujemy je (tripwire assert_dictionary_empty zostaje ścisły dla realnie nowych kluczy). - Jezyk.DoesNotExist (książki): surowy Jezyk.objects.get bez fallbacku → przejście na odporny pobierz_jezyk. - KeyError 'mainLanguage' (artykuły): pop bez default → pop(..., None). - KeyError 'publisher' (książki/rozdziały): brak wydawcy w PBN → wydawca=None (FK jest nullable). Domyślny język gdy PBN nie poda języka albo poda kod spoza słownika: - get_jezyk_polski() + pobierz_jezyk(..., domyslny_jezyk) — deterministycznie polski zamiast "pierwszego rekordu w tabeli". - Parametr domyslny_jezyk przewleczony przez importuj_publikacje_po_pbn_uid_id do artykułów/książek/rozdziałów. - Wybór języka na formularzu nowego importu: pole "Domyślny język publikacji" → config["default_jezyk_id"] → resolve_default_jezyk(session) → PublicationImporter.default_jezyk (analogicznie do default_jednostka). TDD: 6 testów resolvera języka, 3 resolve_default_jezyk, 1 wiring + aktualizacja asercji przekazywania kwargs. Zielono: pbn_integrator (121), pbn_import (325). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…picker aktualnie zatrudnionych Powielanie nagłówka "✅ Autorzy z naszej uczelni" brało się z optgroupów emitowanych per strona AJAX Select2 (przy przewijaniu nagłówek się dublował). - PublicAutorAutocomplete (raporty, ranking, multiseek, raport slotów): płaska lista bez optgroup, WYŁĄCZNIE autorzy z aktualnej uczelni (bieżąca lub historyczna afiliacja) — także w single-install, gdzie mixin scope'ujący jest no-op. Staffowy AutorAutocomplete (admin) zostaje z grupami i autorami zewnętrznymi (potrzebne przy dodawaniu współautorów). - AutorZUczelniAutocopmlete → AutorAktualnieZatrudnionyNaUczelni: zawęża WYŁĄCZNIE po aktualna_jednostka__uczelnia (aktualnie zatrudnieni), płasko, bez żadnych innych warunków. Nazwa klasy oddaje semantykę; URL (autor-z-uczelni-autocomplete) bez zmian, więc admin prac doktorskich/ habilitacyjnych działa bez modyfikacji. TDD: 3 nowe testy (publiczny płaski, single-install tylko-uczelnia, aktualnie zatrudnieni). Zielono: 143 testy autocomplete, manage.py check, ruff. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…e publikacji Usuwa Wydawnictwo_Ciagle i/lub Wydawnictwo_Zwarte z pod-rekordami, zostawiając słowniki (Zrodlo/Wydawca/Jezyk). Flagi --ciagle / --zwarte (domyślnie oba), --dry-run (podgląd planu), oraz bezpiecznik nieinteraktywny --yes-i-am-sure --confirm-db <nazwa_bazy>. Sprząta też generyczne referencje (GFK) i loguje kaskady Django. Zastępuje dawną komendę wyczysc_baze. TDD: 3 testy (kasowanie publikacji + dzieci, zachowanie słowników, dry-run). Zielono, ruff czysty. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Gejt "Lint changed files" padał na 2 błędach ruff, przez co krok nie dochodził do ruff-format (sekwencyjny run:), maskując latentne błędy formatowania. - jednostka.py: usunięty nieużywany import DjangoQLSearchMixin (admin używa BppDjangoQLSearchMixin) — F401. - models/abstract/__init__.py: posortowany blok importów (.titles na właściwe miejsce alfabetyczne) — I001. - 8 plików testowych pbn_import: ruff-format (zawijanie do 88 znaków), czysto kosmetyczne, bez zmian logiki. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… CI)
test_consumers.py (SYNC testy) woła korutyny konsumera przez async_to_sync.
asgiref odmawia, gdy wątek wołającego ma już DZIAŁAJĄCY event loop. Na sharded
CI wcześniejszy test async w tym samym shardzie zostawia działający loop, więc
shardy 2 i 3 deterministycznie wywalały test_connect_* / test_*has_permission
("You cannot use AsyncToSync in the same thread as an async event loop").
Lokalnie, w izolacji, nie reprodukowało się.
Fix: module-local shim async_to_sync uruchamia wywołanie w świeżym wątku (bez
działającego loopa), drop-in dla istniejących miejsc wywołań. Zweryfikowane
symulacją (wywołanie z wnętrza działającego loopa: raw rzuca, shim działa) +
8/8 testów konsumera lokalnie.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Dwa błędy z importu PBN (sesja #10) zatrzymywały całą sesję importu: 1. KeyError 'mode' w importuj_openaccess — blok openAccess z PBN bywa niekompletny (potrafi nie zawierać license/mode/releaseDateMode/ textVersion). Pola docelowe to nullable FK, więc brak klucza zostawia pole puste zamiast wywalać import. Wartość OBECNA, lecz nieznana w słowniku BPP, dalej zgłasza błąd (realna luka konfiguracji). 2. AssertionError {'originalLanguage': 'pol'} w importuj_artykul — adapter eksportu zapisuje originalLanguage z jezyk_orig, ale importer nigdy tego klucza nie odczytywał → leftover wywalał assert_dictionary_empty. Nowy helper ustaw_jezyk_oryginalny mapuje originalLanguage → jezyk_orig (round-trip eksportu), wpięty w import artykułów i książek. Nieznany kod języka → None (jezyk_orig jest nullable, dotyczy tłumaczeń). Refactor: blok dat OA wydzielony do _importuj_openaccess_daty (złożoność cyklomatyczna importuj_openaccess wracała ponad limit ruff C901). Testy: test_openaccess_i_jezyk_orig.py — 10 testów (komplet pól, brak mode, pusty blok, brak openAccess, nieznana licencja → ValueError, daty ISO i rok+miesiąc, mapowanie/nieznany kod/brak originalLanguage). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…acja site (#336) * fix(multihosted): create_from_string czyta widoczność z uczelni z requestu AutorManager.create_from_string przyjmuje teraz jawną uczelnię i czyta z niej nowy_autor_z_formularza_pokazuj; AutorAutocomplete.create_object przekazuje uczelnię z requestu (get_for_request). Bez uczelni fallback na get_single_uczelnia_or_none zamiast first() (pierwsza-z-brzegu). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(multihosted): admin AutorForm afiliuje-default bez first() Inline-owy formularz autora (generuj_formularz_dla_autorow) nie ma requestu, więc domyślne 'afiliuje' bierze z get_single_uczelnia_or_none (single → jej ustawienie; 0/>1 → neutralny default True) zamiast Uczelnia.objects.first(). Sprząta też nieaktualne wpisy bpp/admin/core.py i bpp/models/autor.py z APPROVED_FIRST w guard-teście (nie ma tam już first()). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(multihosted): multiseek IndexCopernicus respektuje uczelnię z requestu Ujednolicono sygnaturę option_enabled(request=None) w hierarchii BppMultiseekVisibilityMixin; enabled() przekazuje request niżej. IndexCopernicusQueryObject ustala uczelnię przez get_for_request(request) (sam degraduje do jedyna-albo-None), więc pole 'Index Copernicus' honoruje pokazuj_index_copernicus oglądanej uczelni zamiast zawsze być widoczne. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(multihosted): single GUI path kolejki PBN zapisuje uczelnię z requestu sprobuj_utworzyc_zlecenie_eksportu_do_PBN_gui przekazuje teraz uczelnia=get_for_request(request) do sprobuj_utowrzyc_wpis (jak batch path), żeby wysyłka w tle użyła właściwej konfiguracji PBN zamiast zgadywać. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(multihosted): link 'Otwórz w PBN' używa uczelni z requestu sprobuj_wyslac_do_pbn przekazuje uczelnia=uczelnia do obu wywołań link_do_pbn() (common.py:146,168). Wcześniej bez argumentu przy >1 uczelni link degradował do None (get_single_uczelnia_or_none → None). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix(multihosted): PublikacjaInstytucji_V2.link_do_pi nie wybucha przy >1 uczelni Fallback self.uczelnia or Uczelnia.objects.get() → get_single_uczelnia_or_none. Martwy guard 'if uczelnia is not None' ożywiony: przy 0/>1 uczelni link się nie renderuje zamiast rzucać MultipleObjectsReturned / DoesNotExist. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * feat(multihosted): admin UczelniaForm wymaga site z przyjaznym komunikatem site jest NOT NULL od migracji 0417; forma dostarcza teraz komunikat dziedzinowy (domena/Site wiąże uczelnię z adresem) zamiast generycznego 'To pole jest wymagane.'. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.