Skip to content

Feature/multi hosted config#189

Open
mpasternak wants to merge 258 commits into
devfrom
feature/multi-hosted-config
Open

Feature/multi hosted config#189
mpasternak wants to merge 258 commits into
devfrom
feature/multi-hosted-config

Conversation

@mpasternak

Copy link
Copy Markdown
Member

No description provided.

mpasternak and others added 30 commits April 28, 2026 18:03
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.
241 commits, ~530 plików. Multi-hosted nadrzędne nad zmianami dev.

Konflikty rozstrzygnięte:

- src/bpp/util.py (UD): dev rozbił monolit na pakiet bpp/util/.
  site_url_for_request() przeniesiona do bpp/util/bpp_specific.py.

- src/bpp_setup_wizard/views.py (UD): dev przepisał setup wizard na
  django-first-run-wizard. Nadpisujemy on_complete() w UczelniaSetupStep
  żeby przekazać request do form.save(request=request).

- src/bpp/admin/helpers/constance_field_mixin.py (UU): dev'owy bool()
  workaround dla constance bezprzedmiotowy (Phase 1 wycofało constance).

- src/bpp/views/browse.py (UU): multi-host filtrowanie news przez
  siteblog.Article.sites M2M (Q(sites=site)|Q(sites__isnull=True)).
  recently_updated/recent_abstracts/total_rekord_count nadal per uczelnia.

- src/miniblog/ (UU): apka to pusta wydmuszka dla historii migracji
  (cutover do siteblog na dev'ie). Nasze migracje 0003-0005 wywalone.

- src/deduplikator_autorow/admin.py (UU): dev przemianował IgnoredAuthor
  → IgnoredScientist + dodał nowy IgnoredAuthor. has_module_permission
  (superuser-only) na obu Admin klasach.

- src/przemapuj_prace_autora/test_integration.py (UU): cache.delete dla
  per-site kluczy (Phase 5) + invalidate_all() (cacheops query cache).

- src/zglos_publikacje/forms.py (UU): łączenie z dev'owym wizard
  rewrite — uczelnia kwarg w nowej sygnaturze __init__.

- src/zglos_publikacje/models.py (UU): clean() używa self._uczelnia
  z fallbackiem (Phase 6.2-6.4).

- src/bpp/tests/test_multisite/test_isolation.py: 3 testy przepisane
  z miniblog.Article.uczelnie → siteblog.Article.sites.

Migracja merge:
- 0418_merge_20260521_1015.py: łączy 0416_rename_dynamic_columns_to_admin
  (dev) i 0417_ensure_uczelnia_site_not_null (multi-host).

Pre-commit fix-ups (ręcznie, manual):
- .github/workflows/docs.yml (z dev): persist-credentials: false
  (zizmor artipacked).
- .pre-commit-config.yaml: mkdocs.yml do check-yaml exclude
  (pymdownx.slugs.slugify python tag); 3 templaty z dev
  (rozbieznosci_if, rozbieznosci_pk, snapshotodpiec_list) do djlint
  exclude (orphan-tag pattern w if/else).
- src/django_bpp/asgi.py: # noqa: E402 na late imports po
  django_asgi_app = get_asgi_application().
- src/bpp/models/konferencja.py: # noqa: DJ001 na 4 CharField'ach z
  null=True (pre-existing tech-debt z dev'a, wymaga migracji w follow-up).
- src/bpp/migrations/0416_rename_dynamic_columns_to_admin.py: usunięty
  nieużywany `from django.apps import apps as django_apps`.
- src/fixtures/conftest_browser.py: usunięty nieużywany
  `from django.core.exceptions import ImproperlyConfigured`.
- src/zglos_publikacje/tests/test_forms.py: usunięty nieużywany
  `from django.core.files.uploadedfile import SimpleUploadedFile`.
- Whitespace fixes: HISTORY.md + 7 docs/*.md (pre-commit auto-fix).
- Ruff import order: drobne reordering w 7 plikach (third-party przed
  first-party).

Smoke: Django check OK, importy bpp.util.site_url_for_request,
bpp.views.browse, zglos_publikacje.forms, bpp_setup_wizard.steps,
siteblog.Article wszystkie działają.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ia.site NOT NULL)

Po mergu origin/dev w branch feature/multi-hosted-config zostało 5 failures
i 3 errors w testach. Tylko wzorce wymagające adaptacji do nowego stanu
po mergu — same testy są poprawne, ale używały API z przed mergea.

- src/bpp/tests/test_views/test_browse/test_browse.py: test_artykuly i
  test_artykul_ze_skrotem używały `a.uczelnie.set([uczelnia])` (M2M na
  starym miniblog.Article). Po mergu Article to siteblog.Article z M2M
  `sites` (do django.contrib.sites.Site). Zamiana na
  `a.sites.set([uczelnia.site])` — fixture uczelnia ma `.site`
  (OneToOne do Site, mandatory po 0417).

- src/bpp/tests/test_views/test_views_browse.py: 3 testy używały
  `Uczelnia.objects.create(nazwa="X", skrot="X")` — to lata przed 0417
  migracją wymuszającą Uczelnia.site NOT NULL. Zamiana na helper
  `any_uczelnia()` (już użyty wcześniej w tym pliku), który auto-tworzy
  Site i przypina go.

- src/deduplikator_autorow/tests/test_xlsx_orcid_and_pbn_url.py: fixture
  `candidate_with_orcid_and_pbn` używała `Uczelnia.objects.get_or_create`
  bez `site=` w defaults. Dodane `site` (get_or_create na testserver).

Wszystkie 468 testów w merge-targeted suite passuje (test_multisite,
test_middleware, test_views, test_admin/test_site_filtered,
bpp_setup_wizard, zglos_publikacje, deduplikator_autorow, miniblog,
przemapuj_prace_autora).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ia.site NOT NULL)

Demo data generator tworzył Uczelnię bez site, co po migracji 0417
(Uczelnia.site mandatory) wywalało NotNullViolation we wszystkich
testach test_demo_data (28 testów: 7 failures + 21 errors w jednej
fixturze jednostki_fixture która tworzy uczelnię przez ensure_uczelnia).

W kontekście CLI/demo nie ma requestu więc get_current_site nie
zadziała — bierzemy pierwszy Site (zwykle django.contrib.sites
fixture 'example.com'), albo tworzymy 'demo.local' jeśli baza pusta.

Tests: 74 passed (test_demo_data full suite + 2 flaky które przy
okazji się przeszły).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…palety (#238)

* feat: dodano trzy nowe zestawy kolorystyczne frontend dla uczelni

Dodano trzy nowe frontend themes dla BPP, nawiązujące kolorystyką do stron uczelni:

1. Uniwersytet VIZJA (vizja.pl):
   - Szary (#3a3a3a) z żółtymi akcentami (#fbb800)
   - Tło: #f8f8f8
   - Buttons: żółte z czarnym text
   - Links: żółte akcenty (#fbb800)

2. MWSLiT Wrocław (mwsl.eu):
   - Granat (#003688) z pomarańczowymi akcentami (#ff6b35)
   - Tło: #f5f8ff
   - Buttons: pomarańczowe z białym text
   - Links: granatowe z pomarańczem na hover

3. UFAM (ufam.edu.pl):
   - Niebieski (#0056b8, #003688)
   - Tło: #f5f8ff
   - Buttons: niebieskie z białym text
   - Links: niebieskie akcenty

Nowe pliki:
- src/bpp/static/scss/_settings_vizja.scss - ustawienia Foundation dla Vizja
- src/bpp/static/scss/_settings_mwsl.scss - ustawienia Foundation dla MWSL
- src/bpp/static/scss/_settings_ufam.scss - ustawienia Foundation dla UFAM
- src/bpp/static/scss/app-vizja.scss - theme Vizja
- src/bpp/static/scss/app-mwsl.scss - theme MWSL
- src/bpp/static/scss/app-ufam.scss - theme UFAM

Każdy theme importuje odpowiedni _settings_*.scss z kolorami,
a resztę ustawień pobiera z domyślnego settings.scss.

Aby użyć nowego theme, w settings/base.py zmień DJANGO_BPP_THEME_NAME
na odpowiedni plik CSS (scss/app-vizja, scss/app-mwsl, scss/app-ufam).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: run_site buduje assets + nowe themes w COMPRESS_OFFLINE_CONTEXT

1. run_site automatycznie buduje frontend assets (make assets)
   - Nowa metoda _build_assets() wywołuje make assets na początku
   - Opcja --skip-assets dla devs którzy mają aktualny CSS
   - Graceful degradation: błędy assets są tylko warningi

2. Dodano nowe frontend themes do COMPRESS_OFFLINE_CONTEXT:
   - scss/app-vizja.css (Uniwersytet VIZJA - szary z żółtymi akcentami)
   - scss/app-mwsl.css (MWSLiT Wrocław - granat z pomarańczem)
   - scss/app-ufam.css (UFAM - niebieski)

Nowe themes są dostępne dla django-compress do offline
kompresji i cachowania.

Aby użyć nowego theme, zmień DJANGO_BPP_THEME_NAME w settings
na odpowiedni plik CSS (scss/app-vizja, scss/app-mwsl, scss/app-ufam).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat: dodano nowe uniwersyteckie themes do Gruntfile.js

Dodano trzy nowe frontend themes do konfiguracji Grunt:
- vizja: scss/app-vizja.scss → scss/app-vizja.css
- mwsl: scss/app-mwsl.scss → scss/app-mwsl.css
- ufam: scss/app-ufam.scss → scss/app-ufam.css

Te taski są teraz budowane równolegle z resztą themes przez
grunt concurrent:themes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(university-themes): poprawki kolorów i ikon kalendarza

- App-vizja: przyciemnienie złotego koloru z #fbb800 na #d4a000
  dla lepszej czytelności na szarym tle #f8f8f8
- Ikona kalendarza: dodanie override dla .uczelnia__tile aby
  używała koloru z klasy .uczelnia__tile-icon zamiast
  $primary-color (kafe na głównej stronie mają teraz własne kolory)
- Ptaszki dropdown: zmiana hardcoded koloru rgba(44, 62, 80, 0.6)
  na rgba($anchor-color, 0.6) dla spójności ze theme'ami

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* refactor(university-themes): MWSL i UFAM jako samodzielne theme'y Foundation

Rozszerzono _settings_mwsl.scss i _settings_ufam.scss z minimalnej formy
(@import 'settings') do pełnego, samodzielnego setu zmiennych Foundation.
Każdy theme zawiera teraz wszystkie 56 sekcji konfiguracji Foundation
z dostosowanymi kolorami uczelni — dzięki temu zmiany w bazowym
_settings.scss nie wpływają na wygląd theme'ów uczelnianych.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* refactor(university-themes): palety zgodne z brandem + rename UFAM→UAFM

- MWSL: primary #ff6b35→#e35b00, secondary #003688→#002b53 (1:1 z mwsl.eu)
- VIZJA: primary #d4a000→#EFA402, secondary #3a3a3a→#01608C (federacjavizja.pl)
- UAFM (poprzednio UFAM): primary #0056b8→#b41906, secondary #003688→#045595,
  alert #cc4b37→#df1a17 (uafm.edu.pl); zmiana nazwy plików, taska Grunta i
  THEME_NAME w base.py
- Usunięto globalną regułę .fi-calendar { color: $primary-color; } z app-vizja,
  app-uafm, app-mwsl, app-green, app-orange — kolor kalendarza wyciekał na
  cały serwis; teraz kolor pochodzi wyłącznie z modyfikatora
  uczelnia__tile-icon--* na kafelku homepage.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
Migracja docsow z Sphinxa na MkDocs Material (87a76da) pozostawila
kilka rozsianych odwolan do Sphinxa — sprzatamy je.

- Makefile: live-docs -> mkdocs serve (zamiast sphinx-autobuild)
- docs/SECURITY_PRACTICES.md: wyjatek dla live-docs opisany przez
  docs/requirements.txt (mkdocs-material) zamiast sphinx-autobuild
- SECURITY.md + docs/SECURITY.md: HISTORY.rst -> HISTORY.md (plik
  HISTORY.md istnieje od dawna, RST byl martwym odsylaczem)
- bin/scan-deps.sh: przyklad dev-only paczki w komentarzu sphinx -> mkdocs
- AUTHORS.rst -> AUTHORS.md (jedyny pozostaly .rst w repo, niczego
  nie referowal, tresc juz w docs/authors.md ale plik w roocie
  zostawiamy dla widocznosci GitHuba)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ications

1. check-flag hint: na pull_request evencie github.ref_name to
   "<PR>/merge" (np. 189/merge) ktorego workflow_dispatch nie akceptuje
   ("HTTP 422: No ref found for: 189/merge"). Dodane HEAD_REF z
   github.head_ref (nazwa branchu zrodlowego PR-a) + fallback do
   ref_name dla nie-PR eventow.

   Przed: gh workflow run build-docker-images.yml --ref 189/merge  (fail)
   Po:    gh workflow run build-docker-images.yml --ref feature/multi-hosted-config

2. docker/bpp_base/Dockerfile: usuniety martwy COPY z
   src/notifications/static/notifications/js/. src/notifications/ apka
   zostala usunieta na dev w commicie 048c2cf (notifications JS jest
   teraz dostarczane przez pakiet django-channels-broadcast, ktory
   wyladuje pliki z venv w runtime collectstatic). Powodowalo to fail
   docker builda na "failed to compute cache key: ... /src/notifications/
   static/notifications/js: not found".

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Podciągnięcie feature/multi-hosted-config do aktualnego origin/dev
(26 commitów od merge-base). Merge bez konfliktów.
mpasternak and others added 3 commits June 5, 2026 22:10
…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>
Comment thread src/oidc_integration/tests/test_logout.py Fixed
mpasternak and others added 26 commits June 6, 2026 07:43
…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>
* docs(pbn-import): spec rozdzielenia pobierania od przetwarzania

Projekt rozbicia kroków importu PBN na niezależne fazy download/process
(6 krokow rozdzielanych + nowa integracja konferencji), z dwukolumnowym
formularzem, zgodnoscia wsteczna configu i miekkim ostrzezeniem przy
pustym lustrze.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(pbn-import): plan implementacyjny rozdzielenia pobierania od przetwarzania

15 taskow TDD: fazy w ImportStepBase, integruj_konferencje, split 6 importerow,
model faz w step_definitions + resolver zgodnosci wstecznej, ImportManager per
faza, CLI granular+legacy, presety, dwukolumnowy formularz.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(pbn-import): auto-review — popraw blockery w spec i planie

- integruj_konferencje: value_or_none zamiast value() (sentinel '[brak k]'),
  savepoint + except IntegrityError, filter().first() (nie get()).
- Task 7: test ustawia default_jednostka, process() guard na nazwa.
- Task 9: nota bloku migracji kontraktu (9-13 przejsciowo czerwone),
  przepisane 4 zlamane testy step_definitions (16 faz), policzona licznosc.
- Task 12: reverse('pbn_import:presets'); presety wylacznie granularne.
- Spec: results -> jedyny konsument _display_results.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-import): ImportStepBase — fazy download/process i __call__(method)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pbn-import): walidacja method w __call__ + nota o atomowości faz

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-integrator): integruj_konferencje — lustro Conference → BPP Konferencja

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(pbn-integrator): integruj_konferencje — wyklucz DELETED z total, komentarze

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-import): ConferenceImporter — split download/process

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-import): SourceImporter — split download/process

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-import): PublisherImporter — split download/process

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-import): AuthorImporter — split download/process

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pbn-import): ConferenceImporter.process zwraca conferences_imported (spójność) + napraw wrapper test po splicie

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-import): PublicationImporter — split download/process

Zastąp monolityczne run() dwoma metodami:
- download(): pobieranie v1+v2 z PBN do lustra
- process(): import z lustra do BPP (opcjonalnie z kasowaniem)
Zaktualizuj test_publication_import.py — stare testy run() zastąpiono
odpowiednikami dla download()/process().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(pbn-import): usuń redundantny inner import Publication w process()

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-import): StatementImporter — split download/process

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-import): model faz w step_definitions + resolver zgodnosci wstecznej

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-import): ImportManager wykonuje fazy per-metoda, results po result_key

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-import): CLI — granularne flagi faz + legacy aliasy

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(pbn-import): myślniki w nazwach flag --disable-<encja>-<faza>

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pbn-import): presety na kluczach granularnych faz

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-import): dwukolumnowy formularz importu (pobieranie/przetwarzanie)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(pbn-import): tabela kroków importu — wyrównanie kolumn faz

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pbn-integrator): integruj_konferencje — przycinanie zbyt długich pól (odporność importu)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…nie dyscyplin (#348)

* docs(pbn-import): spec — dopasowanie autorów po imieniu/nazwisku + odporne przypisywanie dyscyplin

Projekt rozwiązania trzech wad integracji oświadczeń PBN:
- twardy crash sesji importu na konflikcie dyscyplin → raportowana niespójność,
- przedwczesny raport author_not_found → auto-dopasowanie współautora o tym
  samym imieniu/nazwisku (inne ID) + raport informacyjny,
- auto-zakładanie Autor_Dyscyplina z PBN z procentami (100 / 50:50) i śladem
  w logu/uwagach.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(pbn-import): self-review specu — domknięcie 4 ustaleń

- F1: gałąź konfliktu vs bezwarunkowy rec.save() (statements.py:299) — nie
  zostawiać rec.dyscyplina ustawionej na D, by zapis nie utrwalił niespójnej
  pary (autor, dyscyplina) (save() nie woła clean()).
- F2: cykl typów raportów — author_not_found (126) usunięty, author_auto_fixed
  (236) → author_matched_by_name, no_override_without_disciplines/manual_fix
  zostają.
- F3: update_or_create osłonięty savepointem (transaction.atomic).
- F4: konsolidacja powtórzonych elem.get_bpp_discipline().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(pbn-import): plan implementacji — dopasowanie autorów + odporne dyscypliny

4 taski TDD: helper przypisz_dyscypline_pbn (Task 1), wpięcie + koniec crasha
(Task 2), raporty dopasowania autora (Task 3), weryfikacja + pre-commit (Task 4).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-import): helper przypisz_dyscypline_pbn — slot-aware, auto-procenty

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(pbn-import): code-review helpera dyscyplin — get_or_create, update_fields, typy

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pbn-import): konflikt dyscyplin nie wywala importu — raport zamiast raise

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(pbn-import): lock-in — PBN zglasza subdyscypline autora => rec=sub, brak konfliktu

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pbn-import): dopasowanie autora po nazwisku zamiast falszywego author_not_found

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pbn-import): author_matched_by_name action_taken bez klamliwej podmiany autora

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(pbn-import): lock-in braku swapu autora + ruff format

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* style(pbn-import): ruff format test_dyscypliny

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(pbn-import): rejestracja nowych typow niespojnosci w konsumencie + auto-assign test

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(pbn-import): sprostuj przesłankę save()+clean(), dopisz decyzje (no-swap, Task5, follow-up)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(pbn-import): tier-4 znormalizowane dopasowanie bez podmiany autora (spójnie)

Usuwa rec.autor=aut + mid-flow rec.save() (walidujący => ten sam utajony crash)
oraz raport author_replaced; author_matched_by_name leci wspólnie dla tier 2/3/4.
Dodaje odznakę admina dla historycznych author_replaced.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onfig

# Conflicts:
#	.test_durations
#	src/bpp/admin/jednostka.py
#	src/bpp/admin/wydzial.py
#	src/bpp/views/browse.py
#	src/zglos_publikacje/forms.py
#	src/zglos_publikacje/views.py
…_trigger_v3)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…yjątków)

CodeQL przypisał te alerty do PR #189, bo diff jest duży (507 plików) —
realnie kod istniał już na dev. Hardening przy okazji dotknięcia plików:

- pbn_api: redirect z OAuth state.originalPage walidowany przez
  url_has_allowed_host_and_scheme (jedyny realny open-redirect — state
  round-tripuje przez przeglądarkę, jest attacker-controllable)
- browse: redirect paginacji ograniczony do bieżącego hosta
- base_site.html: encodeURIComponent na wartości selecta w location.href
- importer_autorow_pbn / pbn_wysylka_oswiadczen: pełne szczegóły wyjątku
  do Rollbara, do klienta tylko ogólny komunikat (str(e) nie wycieka)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
url_has_allowed_host_and_scheme jest barrier-guardem tylko dla zwalidowanej
wartości; poprzedni fallback `redirect_url = request.path` ponownie wprowadzał
remote-source, więc CodeQL nadal flagował redirect(). Fallback to teraz stała
"/" — sink widzi wyłącznie zwalidowane-lub-literalne dane. Zachowanie bez
zmian (skonstruowany URL jest same-origin, więc guard i tak zawsze przechodzi).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onfig

# Conflicts:
#	src/bpp/models/cache/rekord.py
#	src/bpp/tests/test_cache/test_cache_pk_filter.py
…ularza importu

Kolumny Pobieranie/Przetwarzanie oraz colspan=2 kroków niepodzielnych
w modalu Konfiguracja importu — text-align: left zamiast center.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…filiacja, lint)

Po merge'u dev do feature/multi-hosted-config CI pokazywał 4 grupy błędów —
wszystkie naprawione:

- migracje: dwa liście 0431 (0431_merge_20260612_1504 + dev-owy
  0431_search_index_gin) blokowały migrację testowej bazy → KAŻDY test z DB
  wybuchał. Dodana migracja-merge 0432 (pusta, tylko zależności). To też
  naprawiło dspace_api test_adapter_patent (był ofiarą, nie przyczyną).

- admin eksport jednostki: JednostkaAdmin ma ImportMixin + EksportDanychMixin;
  import_export wybiera import_export_change_list_template po MRO, a ImportMixin
  jest pierwszy → renderował szablon TYLKO z importem (znikał przycisk
  "Eksport"). Wymuszony połączony szablon change_list_import_export.html
  (rozszerza grappelli_mptt jako bazowy → draggable MPTT zachowany).
  Naprawia test_xlsx_export_data[jednostka] + _order_and_freeze_panes.

- playwright test_admin_domyslnie_afiliuje_istniejacy_rekord: baker.make(
  Jednostka) tworzył DRUGĄ uczelnię przez FK → get_single_uczelnia_or_none()
  zwracał None (>1) → default afiliacji degradował do True → asercja padała
  dla expected=False. Jednostka przypięta do uczelni z testu (1 uczelnia).
  Bug testu odsłonięty przez (poprawną) semantykę multi-hosted.

- lint: ruff format na pbn_integrator/tests/test_openaccess_i_jezyk_orig.py
  (jedyny zmieniony plik poza extend-exclude).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
pip-audit blokował CI na 7 CVE w 2 runtime-pakietach:
- django 5.2.14 -> 5.2.15: 5x PYSEC-2026 (197-201)
- pypdf 6.10.2 -> 6.13.2: CVE-2026-48155, CVE-2026-48156 (fix >=6.12.0;
  pypdf jest tranzytywne przez xhtml2pdf)

Podniesiony też floor Django w pyproject (>=5.2.15) żeby wymusić patcha
security. pytest 8.4.2 CVE-2025-71176 NIE ruszany — to dev-dep (poza
skanem runtime CI) i jest przypięty <9 przez pytest-testcontainers-django.

Walidacja lokalna: manage.py check czysty, nowe_raporty (ścieżka
xhtml2pdf->pypdf) 61 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Użytkownik zalogowany przez Keycloak (OIDC) widział w menu link
„zmiana hasła" → /password_change/, który kończył się błędem 500.
Przyczyna: SmartPasswordChangeView robił EXTERNAL_PROVIDER_URLS[backend],
a backend OIDC był w EXTERNAL_AUTH_BACKENDS, ale nie w mapie URL-i → KeyError.

- Link pokazujemy per-użytkownik: tylko zalogowanym hasłem BPP (Django
  ModelBackend), nie przez OIDC/Microsoft/ORCID. Nowy context processor
  external_auth_status → zmienna logged_in_via_external_auth; top_bar.html
  używa jej zamiast globalnego microsoft_login_enabled.
- SmartPasswordChangeView._external_provider_info(): odporny lookup zamiast
  [backend]; OIDC dostaje przyjazną stronę z linkiem do panelu konta
  Keycloaka (OIDC_ACCOUNT_CONSOLE_URL = {issuer}/account), szablon pokazuje
  przycisk tylko gdy URL jest ustawiony.

Testy: 11 nowych (context processor + widok, w tym reprodukcja 500 → 200).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
…onfig

# Conflicts:
#	.github/workflows/docs.yml
#	uv.lock
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants