diff --git a/docs/administrator/ogolna.md b/docs/administrator/ogolna.md index 58644fae9..3a8907c56 100644 --- a/docs/administrator/ogolna.md +++ b/docs/administrator/ogolna.md @@ -66,6 +66,43 @@ Jeżeli chcesz później schować lub pokazać takie formularze, skorzystaj z op Redagowanie➡Administracja➡Formularze wyszukiwania. Kliknij nazwę takiego formularza, następnie zaznacz lub odznacz opcję "Publiczny" i zapisz rekord +#### Układ podstrony autora (profil autora) {#układ-podstrony-autora-profil-autora} + +W rekordzie uczelni (Redagowanie➡Struktura➡Uczelnia, edycja rekordu) znajduje +się zwijana sekcja **„Profil autora (podstrona)"**. Zawiera ona **kafelkowy +edytor układu** prawej kolumny publicznych podstron autorów (patrz +[opis strony autora w instrukcji użytkownika](../uzytkownik/przegladanie-i-wyszukiwanie.md#strona-autora)). +Ustawienie jest **globalne dla całej uczelni** — obowiązuje na podstronach +wszystkich autorów. + +Edytor działa następująco: + +- każda dostępna sekcja prawej kolumny to osobny **kafelek**, +- **przeciągnij** kafelki (ang. *drag and drop*), aby zmienić ich kolejność, +- **odznacz checkbox** przy kafelku, aby ukryć daną sekcję, +- dla sekcji listowych ustaw **limit** liczby wyświetlanych pozycji + (do wyboru 10, 20, 30 lub 50). + +!!! note + Lewa kolumna podstrony autora (zdjęcie, biogram, identyfikatory, + historia zatrudnienia, formularz wyszukiwania, przyciski eksportu itd.) + jest **stała** i nie podlega konfiguracji w tym edytorze. + +!!! tip + **Pusty układ oznacza układ domyślny** — system pokaże rozsądny, + wbudowany zestaw sekcji. Nie trzeba niczego konfigurować, jeśli domyślny + układ jest wystarczający. + +Wśród dostępnych sekcji prawej kolumny znajdują się m.in.: statystyki wg +charakteru formalnego, wykresy roczne (liczba publikacji / suma punktów MNiSW / +suma Impact Factor), najczęstsi współautorzy, najlepsze prace (wg punktacji +oraz wg Impact Factor), najnowsze artykuły, najnowsze książki i rozdziały, +ostatnio edytowane rekordy, dyscypliny, źródła oraz wyróżnione publikacje. + +!!! note "Zrzut ekranu" + W tym miejscu można w przyszłości umieścić zrzut ekranu kafelkowego + edytora układu podstrony autora. + ## Sposób kalkulacji dyscyplin 1. Dyscyplina i subdyscyplina naukowa przypisana autorowi na dany rok zawiera się w rekordzie `Autor_Dyscyplina`. diff --git a/docs/redaktor/autorzy.md b/docs/redaktor/autorzy.md index 7b336aaf1..31b9b95e4 100644 --- a/docs/redaktor/autorzy.md +++ b/docs/redaktor/autorzy.md @@ -29,3 +29,43 @@ Algorytm ustalania aktualnego miejsca pracy działa w sposób następujący: !!! warning dany autor może mieć tylko jedno powiązanie oznaczone jako *Podstawowe miejsce pracy*. + +## Zdjęcie i biogram autora (samodzielna edycja) + +Zalogowany użytkownik powiązany ze swoim rekordem autora może **samodzielnie** +zredagować dane prezentowane na jego publicznej +[stronie autora](../uzytkownik/przegladanie-i-wyszukiwanie.md#strona-autora) — +bez pośrednictwa redaktora. Edycji podlegają: + +- **biogram** — wpisywany w języku Markdown lub w HTML, z **podglądem na żywo** + (zmiany widać od razu obok pola edycji), +- **zdjęcie** — wgrane zdjęcie jest automatycznie kadrowane do kwadratu + i skalowane, zapisywane w formacie WebP; maksymalny rozmiar pliku to 5 MB. + +Wejście do edycji: na górnej belce wybierz **„Mój profil"**, a następnie +**„Edytuj swoją stronę"** (adres `/bpp/profil/edycja/`). + +!!! note + Te same pola (zdjęcie i biogram) redaktor może ustawić w module + redagowania, w rekordzie autora — przydatne, gdy autor nie edytuje + swoich danych samodzielnie. + +## Wyróżnione publikacje autora + +Autor może wskazać i uszeregować **wyróżnione prace**, które pojawią się na +początku jego [strony autora](../uzytkownik/przegladanie-i-wyszukiwanie.md#strona-autora) +— pod warunkiem, że uczelnia włączyła sekcję „wyróżnione publikacje" w układzie +podstrony (patrz +[układ podstrony autora](../administrator/ogolna.md#układ-podstrony-autora-profil-autora)). + +Listę wyróżnionych prac edytuje się na stronie +`/bpp/profil/edycja/wybrane-publikacje/`: + +- wyszukiwarka podpowiada pozycje do dodania, +- prace można **dodawać**, **usuwać** oraz **zmieniać ich kolejność** + (strzałkami). + +!!! note + Wyszukiwarka podpowiada **wyłącznie własne prace autora** — autor może + wyróżnić tylko publikacje, z którymi jest powiązany. + diff --git a/docs/superpowers/specs/2026-06-18-profil-autora-i-podstrona-design.md b/docs/superpowers/specs/2026-06-18-profil-autora-i-podstrona-design.md new file mode 100644 index 000000000..284a67803 --- /dev/null +++ b/docs/superpowers/specs/2026-06-18-profil-autora-i-podstrona-design.md @@ -0,0 +1,309 @@ +# Profil autora (edytowalny) + przebudowa podstrony autora — projekt + +Data: 2026-06-18 +Status: zatwierdzony do napisania planu implementacji + +## 1. Cel + +Dwie powiązane zmiany: + +1. **Przebudowa publicznej podstrony autora** (`/autor//`, `/autor//`) — + z dotychczasowej strony „metadane + formularz wyszukiwania" na stronę + złożoną z **konfigurowalnych sekcji treści** (kolejność + widoczność + + limity), renderowanych nad spójnym, stałym nagłówkiem tożsamości. +2. **Edytowalny profil autora** — autor (zalogowany użytkownik powiązany z + rekordem `Autor`) może wgrać zdjęcie i biogram oraz ułożyć własną stronę: + wybrać które sekcje, w jakiej kolejności i w jakiej liczbie pozycji mają się + wyświetlać. + +Dostarczane **fazowo**: + +- **Faza 1 (PR1):** model danych + render publicznej strony + edycja w Django + adminie. Wszyscy autorzy bez konfiguracji dostają układ domyślny. +- **Faza 2 (PR2):** self-service edytor w „Mój profil" (drag-drop, uploady, + live preview, picker wyróżnionych prac) + eksport RIS. + +## 2. Stan obecny (ustalenia ze zwiadu po kodzie) + +- **Publiczna strona:** `AutorView(DetailView)` — `src/bpp/views/browse.py:145`, + szablon `src/bpp/templates/browse/autor.html`, URL-e `src/bpp/urls.py:262,288` + (oba `bpp:browse_autor`). Dziś renderuje metadane + formularz „Wyszukaj + publikacje autora" (POST → `bpp:browse_build_search` → multiseek) + kod do + embedowania. Nie listuje publikacji inline. +- **„Mój profil":** `ProfilUzytkownikaView(LoginRequiredMixin, TemplateView)` — + `src/bpp/views/profile.py:5`, URL `profil/` (`bpp:profil-uzytkownika`), + szablon `src/bpp/templates/bpp/profil_uzytkownika.html`. **Wyłącznie + read-only** — brak jakiegokolwiek formularza edycji. +- **Powiązanie User↔Autor:** `BppUser.autor` OneToOneField (`related_name="user"`) + — `src/bpp/models/profile.py:56-64`. Auto-dopasowanie po e-mailu/nazwisku: + `BppUser.sprobuj_dopasowac_autora()` (`profile.py:80-115`). +- **Model `Autor`:** `src/bpp/models/autor.py:81`. Ma `opis` + (`HTMLField`/TinyMCE, `autor.py:129`) + `pokazuj_opis` — pokazywany w + nagłówku jako „Opis". **Brak** jakiegokolwiek pola na zdjęcie. Pillow + zainstalowany; wzorzec ImageField: `Uczelnia.logo_www` + (`src/bpp/models/uczelnia.py:96`, `upload_to="logo"`). +- **Dane publikacji:** zdenormalizowany cache `Rekord` + (`src/bpp/models/cache/rekord.py`), `RekordManager.prace_autora(autor)` + (`rekord.py:45`) → `filter(autorzy__autor=autor).distinct()`. Pola: `rok`, + `charakter_formalny`, `zrodlo` (`rekord.py:217`, FK `bpp.Zrodlo`), + `ostatnio_zmieniony` (`rekord.py:231`), punktacja z `ModelPunktowanyBaza` + (`punkty_kbn`, `impact_factor`, ... — `src/bpp/models/abstract/scoring.py`, + wszystkie indeksowane). +- **Punkty autora:** `Cache_Punktacja_Autora` (`src/bpp/models/cache/punktacja.py:51`, + `managed=True`), pola `autor`, `rekord_id` (TupleField), `pkdaut`, `slot`. +- **Charakter pracy:** `Charakter_Formalny` (MPTT) — `charakter_ogolny` + (`art`/`roz`/`ksi`/`xxx`, `src/bpp/const.py:32-35`) rozróżnia + artykuł/rozdział/książkę/inne; modele źródłowe `Wydawnictwo_Ciagle` + (artykuły) i `Wydawnictwo_Zwarte` (zwarte). +- **Dyscypliny:** `Autorzy.dyscyplina_naukowa` (`src/bpp/models/cache/autorzy.py:22,52`, + FK `bpp.Dyscyplina_Naukowa`). +- **Współautorzy:** `AuthorConnection` (`src/powiazania_autorow/models.py:6`) — + `primary_author`, `secondary_author`, `shared_publications_count`, + `ordering=["-shared_publications_count"]`. Stored undirected (para + uporządkowana po id), więc filtrować po obu stronach. Prekomputowany + okresowo (Celery). **To dokładnie ten sam obiekt, który zasila wizualny + browser „powiązania autorów 2D/3D"** (URL-e `/autor//powiazania/`, + `/powiazania/3d/`, JSON-y — `powiazania_autorow.views`, gejtowane przez + `czy_pokazywac_siec_powiazan` w `browse.py:150-163`). +- **Raport autora:** `nowe_raporty`, slug `raport-autorow`, pk-linkowalny + (`/nowe_raporty/raport-autorow////`), + `nowe_raporty:raport_form` / `:raport_generuj` (`src/nowe_raporty/urls.py`). + Widoczność: `DefinicjaRaportu.widoczny_dla(request)` + (`src/nowe_raporty/models.py:115-128`); domyślnie publiczny, admin-konfigurowalny. +- **Sanityzacja / Markdown:** `nh3` użyty w `bpp.util.text.safe_html()` + (`src/bpp/util/text.py:168`) — idiomatyczny sanitizer repo. Pakiet `markdown` + 3.10.2 zainstalowany (importowalny), bez istniejącego pipeline'u renderu. +- **Eksport cytowań:** BibTeX istnieje i jest reużywalny dla listy prac + (`src/bpp/export/bibtex.py`, `export_to_bibtex()` `bibtex.py:507`, metody + `.to_bibtex()` na modelach) — operuje na konkretnych obiektach + `Wydawnictwo_*`/`Patent`/`Praca_*` (z `Rekord` przez `.original`). + **RIS nie istnieje — net-new.** +- **Wyróżnione/wybrane publikacje:** koncept **nie istnieje** + (`Autorzy.przypieta` dotyczy przypinania dyscypliny do PBN, nie wyróżniania + prac). **Net-new model.** + +## 3. Decyzje projektowe (zatwierdzone) + +| Zagadnienie | Decyzja | +|---|---| +| Biogram | Nowe pola `biogram` + `biogram_format` (md/html). `opis` nietknięty. | +| Kto edytuje (Faza 2) | Każdy zalogowany z ustawionym `user.autor`. | +| Dostawa | Fazowo: Faza 1 render + admin, Faza 2 self-service. | +| Bloki metadanych | Zostają stałym nagłówkiem; edytor steruje tylko sekcjami treści. | +| Ranking „best" | Osobno `najlepsze_pk` (`-punkty_kbn`) i `najlepsze_if` (`-impact_factor`). | +| Limit list | 10/20/30/50, domyślnie 10, per sekcja listowa. | +| Zdjęcie | Awatar w nagłówku, kwadrat 400×400 (center-crop), zapis **WebP** q≈85, ≤5 MB. | +| Statystyki | Liczba prac wg charakteru formalnego (szczegółowo). | +| Link do raportu | 3 linki: bieżący rok, ostatnie 4 lata, szczegółowy formularz; widoczność wg `widoczny_dla`. | +| Układ domyślny | „biogram-najpierw". | +| Domyślne sugestie | ON: `wykres_lata` + `wspolautorzy`; reszta sugestii OFF. | +| `opis` vs `biogram` | `opis` zostaje w nagłówku; biogram to osobna sekcja. | +| Współautorzy | `AuthorConnection` (prekomputowany) + CTA do browsera 2D/3D. | + +## 4. Model danych (Faza 1) + +Nowa migracja w `src/bpp/migrations/` (NIE edytować istniejących). + +### 4.1. Nowe pola na `Autor` (`src/bpp/models/autor.py`) + +- `zdjecie = models.ImageField(upload_to="autor_zdjecia", null=True, blank=True)` + — przetwarzane przy zapisie do kwadratu 400×400 WebP (patrz 7.3). +- `biogram = models.TextField(blank=True, default="")` — surowe źródło. +- `biogram_format = models.CharField(max_length=4, choices=[("md","Markdown"), + ("html","HTML")], default="md")`. +- `uklad_profilu = models.JSONField(null=True, blank=True, default=None)` — + `null` = układ domyślny; inaczej lista pozycji (patrz 6). + +### 4.2. Nowy model `WybranaPublikacjaAutora` + +Wyróżnione prace (ręcznie wybierane). Plik: `src/bpp/models/wybrana_publikacja.py` +(zarejestrowany w `src/bpp/models/__init__.py`). + +``` +autor = FK(Autor, related_name="wybrane_publikacje", on_delete=CASCADE) +content_type = FK(ContentType, on_delete=CASCADE) +object_id = PositiveIntegerField +publikacja = GenericForeignKey("content_type", "object_id") +kolejnosc = PositiveIntegerField(default=0) +class Meta: ordering = ["kolejnosc"]; unique_together = [(autor, content_type, object_id)] +``` + +Rozwiązywane do `Rekord` przez `(content_type_id, object_id)` lub do `.original`. +W Fazie 1 wypełniane przez admina (inline); w Fazie 2 przez picker self-service. + +## 5. Rejestr sekcji (w kodzie) + +Plik: `src/bpp/profil/sekcje.py`. Katalog typów sekcji żyje w kodzie; per-autor +JSON trzyma tylko kolejność/widoczność/limit. Dodanie sekcji = zmiana kodu, bez +migracji danych. + +Każdy wpis rejestru: `klucz`, `nazwa`, `obowiazkowa: bool`, `ma_limit: bool`, +`domyslnie_widoczna: bool`, `template` (partial), funkcja +`pobierz_kontekst(autor, limit, request) -> dict|None` (zwraca `None`/pusty → +sekcja auto-ukryta). + +| klucz | nazwa | obow. | limit | dom. ON | źródło | +|---|---|---|---|---|---| +| `biogram` | Biogram | nie | – | tak | `autor.biogram_html` | +| `wyszukiwarka` | Wyszukiwarka prac | **tak** | – | tak (wymuszone) | obecny POST→multiseek | +| `najlepsze_pk` | Najlepsze prace (punkty MNiSW) | nie | tak | tak | `prace_autora` `-punkty_kbn` | +| `najlepsze_if` | Najlepsze prace (Impact Factor) | nie | tak | tak | `prace_autora` `-impact_factor` | +| `najnowsze_artykuly` | Najnowsze artykuły | nie | tak | tak | wyd. ciągłe `-rok` | +| `najnowsze_zwarte` | Najnowsze książki / rozdziały | nie | tak | tak | wyd. zwarte `-rok` | +| `ostatnio_edytowane` | Ostatnio edytowane | nie | tak | tak | `-ostatnio_zmieniony` | +| `wybrane_publikacje` | Wybrane publikacje | nie | – | nie | `WybranaPublikacjaAutora` | +| `statystyki_charakter` | Statystyki wg charakteru | nie | – | tak | `Count` po `charakter_formalny` | +| `wykres_lata` | Publikacje w latach | nie | – | **tak** | `autor.prace_w_latach` | +| `punkty_lata` | Punkty / sloty w latach | nie | – | nie | `Cache_Punktacja_Autora` | +| `dyscypliny` | Udział dyscyplin | nie | – | nie | `Autorzy.dyscyplina_naukowa` | +| `zrodla` | Najczęstsze źródła / czasopisma | nie | tak | nie | `Rekord.zrodlo` | +| `wspolautorzy` | Najczęstsi współautorzy | nie | tak | **tak** | `AuthorConnection` + CTA 2D/3D | +| `eksport` | Eksport listy publikacji | nie | – | nie | `export_to_bibtex` (BibTeX; RIS w Fazie 2) | + +Wszystkie zapytania listowe: `select_related("charakter_formalny","zrodlo", +"wydawca")`, twardy limit, świadome unikanie N+1. + +## 6. Konfiguracja układu (`uklad_profilu`) i jej rozwiązywanie + +### 6.1. Schemat JSON + +Lista pozycji w kolejności wyświetlania: + +```json +[ + {"klucz": "biogram", "widoczna": true, "limit": null}, + {"klucz": "wyszukiwarka", "widoczna": true, "limit": null}, + {"klucz": "najlepsze_pk", "widoczna": true, "limit": 10}, + ... +] +``` + +`limit ∈ {10,20,30,50}` tylko dla sekcji `ma_limit=True`; w przeciwnym razie +`null`. + +### 6.2. Walidacja (`waliduj_uklad`) + +- Klucze spoza rejestru → odrzucone. +- `limit` poza `{10,20,30,50}` dla sekcji listowej → korekta do 10. +- Sekcja `obowiazkowa` → `widoczna` wymuszone na `True`. + +### 6.3. Rozwiązywanie (`rozwiaz_uklad(autor) -> list[SekcjaUkladu]`) + +1. Start od kanonicznego porządku domyślnego (kolejność z tabeli w §5, + z `domyslnie_widoczna`). +2. Jeśli `autor.uklad_profilu` niepuste: nadpisz kolejność/widoczność/limit dla + znanych kluczy. +3. Sekcje z rejestru nieobecne w zapisanym configu → dołączone w pozycji + kanonicznej z domyślami (forward-compat: nowo dodana sekcja pojawia się + automatycznie). +4. Wymuś `widoczna=True` dla obowiązkowych. +5. Zwróć tylko `widoczna=True`; przy renderze dodatkowo odpadają sekcje, których + `pobierz_kontekst` zwróci pusto (auto-ukrywanie). + +**Układ domyślny** (biogram-najpierw, z domyślnymi sugestiami ON): +`biogram → wyszukiwarka → najlepsze_pk → najlepsze_if → najnowsze_artykuly → +najnowsze_zwarte → ostatnio_edytowane → statystyki_charakter → wykres_lata → +wspolautorzy`. Pozostałe (`wybrane_publikacje`, `punkty_lata`, `dyscypliny`, +`zrodla`, `eksport`) istnieją w rejestrze, domyślnie OFF. + +## 7. Render publicznej strony (Faza 1) + +### 7.1. `AutorView` (`src/bpp/views/browse.py`) + +`get_context_data`: +- `sekcje = rozwiaz_uklad(self.object)` z policzonym kontekstem każdej widocznej + sekcji (leniwie), z auto-ukrywaniem pustych. +- `raport_links` — jeśli `DefinicjaRaportu.objects.filter(slug="raport-autorow")` + istnieje i `.widoczny_dla(request)`: + - bieżący rok: `…/raport-autorow////` + - ostatnie 4 lata: `…/raport-autorow////` + - szczegółowy: `nowe_raporty:raport_form` (slug `raport-autorow`). + `rok = timezone.now().year`. Brak `DefinicjaRaportu` → brak linków (guard). + +### 7.2. Szablony + +- `src/bpp/templates/browse/autor.html` — refaktor: stały nagłówek + (zdjęcie-awatar + nazwisko + jednostka + ORCID/PBN/metryki/stopnie/cytowania — + jak dziś, **`opis` zostaje**) + blok linków raportu + pętla po `sekcje` + renderująca `{% include sekcja.template %}`. +- `src/bpp/templates/browse/autor_sekcje/.html` — partial na sekcję. +- Ikony: frontend publiczny → Foundation-Icons (``). +- `wspolautorzy`: lista top-N (linki do podstron + `shared_publications_count`) + + CTA „Zobacz pełną sieć powiązań" → `bpp:browse_autor_powiazania` / + `…_3d` (gdy `ma_powiazania`). + +### 7.3. Przetwarzanie zdjęcia + +`src/bpp/util/obrazy.py`: `przetworz_zdjecie_autora(plik) -> ContentFile`: +- walidacja ≤5 MB i typu obrazu (na poziomie formularza), +- Pillow: `ImageOps.exif_transpose`, center-crop do kwadratu, resize 400×400, + zapis WebP q≈85. +Wołane z save'a admina (Faza 1) i formularza self-service (Faza 2). Reużywalna +funkcja, jeden punkt prawdy. + +### 7.4. Render biogramu + +`Autor.biogram_html` (`cached_property`): `md` → +`markdown.markdown(biogram)` → `safe_html(...)`; `html` → `safe_html(biogram)`. +Jeden punkt sanityzacji (nh3, `bpp.util.text.safe_html`). + +## 8. Admin (Faza 1) + +`src/bpp/admin/` (admin `Autor`): +- Pola `zdjecie` (z podglądem miniatury), `biogram` + `biogram_format`. +- Edytor układu: formularz listujący sekcje z checkboxem widoczności, polem + kolejności i selectem limitu; zapis do `uklad_profilu` (ta sama logika + walidacji/serializacji reużyta w Fazie 2). +- Inline `WybranaPublikacjaAutora` z autocomplete prac. +- Admin używa emoji (bez Foundation Icons) zgodnie z konwencją repo. + +## 9. Faza 2 (osobny PR) + +- `src/bpp/views/profil_edycja.py` — widok edycji w „Mój profil", gate: + `LoginRequiredMixin` + wymóg `request.user.autor`. +- Edytor: drag-drop kolejności (sprawdzić istniejący JS sortowania w repo przed + dodaniem zależności), przełączniki widoczności, selecty limitów, upload + zdjęcia z podglądem, edytor biogramu z przełącznikiem MD/HTML + **live + preview** (render serwerowy AJAX-em przez ten sam pipeline), picker + wyróżnionych prac (autocomplete add/remove/reorder). +- Eksport zbiorczy: `/autor//eksport.bib` (BibTeX, reużycie + `export_to_bibtex`) i `/autor//eksport.ris` (**RIS net-new**). Świadomy + limit/stream dla autorów z dużą liczbą prac. +- „Mój profil" (`profil_uzytkownika.html`) zyskuje link „Edytuj swoją stronę". + +## 10. Testy (pytest + `model_bakery.baker`, bez unittest) + +- Model: przetwarzanie zdjęcia (rozmiar/crop/format WebP, korekta EXIF), + walidacja ≤5 MB; sanityzacja biogramu (XSS usunięty, MD wyrenderowany, + niedozwolone tagi wycięte); walidacja i rozwiązywanie układu (domyślny vs + override; forward-compat nowej sekcji; wymuszenie obowiązkowej). +- Widok: kolejność sekcji zgodna z configiem; wyszukiwarka zawsze obecna; + auto-ukrywanie pustych sekcji; gating linków raportu (anon bez / uprawniony z); + CTA współautorów tylko gdy `ma_powiazania`. +- Faza 2: gate edycji (obcy autor / brak `user.autor` → odmowa), zapis układu, + eksport BibTeX/RIS. + +## 11. Migracje i baseline + +- Nowa migracja: pola na `Autor` + model `WybranaPublikacjaAutora`. +- Po migracjach (raz, przy scalaniu): `make baseline-update` — odświeżenie + `baseline-sql/baseline.sql` + `baseline.meta.json` (commit obu). Nie odświeżać + w równoległych branchach. + +## 12. Dostawa (worktree + PR) + +Praca w worktree jako siostrzany katalog (zgodnie z CLAUDE.md), **nie** w `bpp/`: + +``` +git worktree add ~/Programowanie/bpp-profil-autora -b feature/profil-autora +``` + +Zmiany trafiają do PR-a (osobny PR na Fazę 1 i Fazę 2). + +## 13. Świadomie poza zakresem (YAGNI) + +- RIS i eksport zbiorczy — dopiero Faza 2. +- Self-service picker / drag-drop / live preview — Faza 2. +- Pole „zainteresowania badawcze" — odrzucone w brainstormingu. +- Własna nowa wizualizacja współautorów — reużywamy istniejący browser 2D/3D. +- Migracja `opis` → `biogram` — `opis` zostaje niezależny. diff --git a/docs/superpowers/specs/2026-06-19-profil-autora-faza2-NEXT-HANDOFF.md b/docs/superpowers/specs/2026-06-19-profil-autora-faza2-NEXT-HANDOFF.md new file mode 100644 index 000000000..7f2fd13d4 --- /dev/null +++ b/docs/superpowers/specs/2026-06-19-profil-autora-faza2-NEXT-HANDOFF.md @@ -0,0 +1,183 @@ +# Profil autora — STATUS po rewizji 2-kol + §3.8 + HANDOFF do następnej sesji + +Data: 2026-06-19 +Dotyczy: PR **#385** (branch `feature/profil-autora` → `dev`). +Poprzednie dokumenty (czytaj dla kontekstu, w tej kolejności): +1. `docs/superpowers/specs/2026-06-19-profil-autora-rewizja-2col-HANDOFF.md` (rewizja 2-kol) +2. `docs/superpowers/specs/2026-06-18-profil-autora-i-podstrona-design.md` (spec bazowy) + +> Ten dokument jest samowystarczalny. Świeża sesja ma z niego wznowić bez +> dostępu do poprzednich rozmów. + +## STATUS — sesja nocna 2026-06-19/20 (autonomiczna, wszystko w PR #385) + +Wykonane tej nocy (TDD, commit po commicie, każdy push z zielonym CI): + +- **§3.1 kalibracja wykresów** — oś Y (etykieta maks + linie siatki) dla + wariantu liniowego i słupkowego. Zweryfikowane wizualnie. Commit `c36147c2b`. +- **§3.3 eksport BibTeX + RIS** — `/bpp/autor//eksport.bib` (reuse + `bpp/export/bibtex.py`) i `/bpp/autor//eksport.ris` (net-new + `bpp/export/ris.py`). Przyciski w lewej kolumnie. Limit `MAKS_EKSPORT=5000`. + Endpointy zweryfikowane na żywo. Commit `0c5b59605`. +- **§3.4 picker wyróżnionych prac** — self-service add/remove/reorder w + `/bpp/profil/edycja/wybrane-publikacje/`, autocomplete ograniczony do + WŁASNYCH prac autora, walidacja własności serwerowo na każdej akcji. + Zweryfikowane na żywo. Commity `d359451a4` + `815fecef5` (polish: striptags). +- **§3.2 edytor kafelkowy układu** — drag-drop (jquery-ui sortable z + Grappelli) w `UczelniaAdmin`: kafelek/sekcja z checkboxem widoczności + + selectem limitu, serializacja do `{"klucz","widoczna","limit"}` + (`waliduj_uklad`). Zweryfikowane wizualnie (14 kafelków). Commit `8bd49089c`. +- **FIX migracji** — `dev` dodał własną `0444_alter_uczelnia_…`, kolidującą + z profilowymi `0444`/`0445` (dwa liście grafu w stanie scalonym z dev, w + którym CI testuje PR). Scalono `dev` (czysto), dodano pustą migrację + scalającą `0446_merge_…`. Commity `ed9b27f41` (merge) + `5358aec94`. + +Stan: 91 testów w `src/bpp/tests/test_profil/` zielonych na świeżym +kontenerze (pod scalonymi zależnościami: pytest 9, denorm 1.12.2). CI „Tests" +zielone na pośrednich pushach. + +NADAL ODŁOŻONE (NIE robione): `make baseline-update` (przy scalaniu PR; teraz +doszły migracje 0444-dev + 0446-merge) oraz samo scalenie PR #385 → dev. +Drobiazg do poprawienia kiedyś: dokumentowany w CLAUDE.md autologin +`/__autologin__/?token=…` zwraca 404 (pakiet `django-dev-helpers` oczekuje +`?__autologin__=`); w tej sesji obejście = mintowanie sesji DB. + +## 0. Jak wznowić (środowisko) + +- Worktree: `~/Programowanie/bpp-profil-autora`, branch `feature/profil-autora`. + **NIE twórz nowego worktree, NIE pracuj w `~/Programowanie/bpp`.** +- HEAD na chwilę pisania: `bb37ebbba`. Working tree czysty. +- Push (SSH nie działa; gh zalogowany jako `mpasternak`): + `git push https://github.com/iplweb/bpp.git feature/profil-autora:feature/profil-autora` +- Testy profilu: `PYTEST_TESTCONTAINERS_REUSE=1 uv run pytest src/bpp/tests/test_profil/` + - Gdy wyskoczy `bpp_uczelnia.site_id NOT NULL` (stale reuse-container) → + `make clean-testcontainers` i odpal bez `REUSE` (świeży kontener OK). +- Czyste logiki (bez DB) szybciej: `PYTEST_TESTCONTAINERS_DISABLE=1 uv run pytest ...` +- Po zmianach SCSS: `grunt build` (skompilowany CSS jest poza gitem — + commituj tylko źródła `.scss`). +- KAŻDY nowy `.py` (też migracje) → `uv run ruff format ` przed commitem; + `pre-commit` (bez argumentów) musi być zielony. + +## 1. Co JEST zrobione (CI w pełni zielone na #385) + +Cała **rewizja 2-kolumnowa §3.1–§3.7** + **self-service §3.8** wdrożone TDD, +commit po commicie. Wszystkie realne gejty zielone (Build test-runner image, +12 shardów Tests, Lint changed files, CodeQL, baseline freshness, vitest), +`mergeStateStatus: CLEAN`. + +Commity tej linii pracy (najnowszy u góry): +- `bb37ebbba` test(fulltext): uniezależnienie testu od danych bazowych +- `353ccb20c` §3.8 self-service edycja biogramu i zdjęcia +- `92f0dd81b` fix CodeQL: stretched-link zamiast JS-nawigacji +- `76338b03a` fix CodeQL: bezpieczna nawigacja (zastąpione przez stretched-link) +- `f8049fd38` §3.1 admin (edytor układu na Uczelni) + SCSS 2-kol +- `06ebb1872` §3.7 historia zatrudnienia (lewa kolumna) +- `2a1ab948c` §3.6 klikalne statystyki wg charakteru → wyszukiwarka +- `065e394d7` §3.5 wykresy roczne (liniowy/słupkowy) + PK + IF +- `d5e3154b7` §3.4 klik w całą pozycję listy prac +- `8ab6c24a2` §3.1+§3.2+§3.3 układ per-Uczelnia + rejestr prawej kol + szablon 2-kol +- (Faza 1: `00264292e`, `65af2e00b`, `9951f7fb5`, `400f87d5c`, …) + +### 1.1. Architektura — gdzie co jest +- Układ profilu = **globalny per-Uczelnia**: `Uczelnia.uklad_profilu_autora` + (JSONField). `Autor.uklad_profilu` USUNIĘTE. Migracja `0445`. +- `bpp/profil_autora.py` — rejestr `KATALOG_SEKCJI` = **tylko PRAWA kolumna** + (15 typów, bez `obowiazkowa`). `rozwiaz_uklad(uczelnia)`. +- `bpp/profil_autora_dane.py` — buildery + `przygotuj_sekcje(autor, uczelnia, + request)`; helper `_agreguj_po_latach`. Nowe buildery `_wykres_pk_lata`, + `_wykres_if_lata` (IF auto-hide gdy suma=0). +- `bpp/views/browse.py` `AutorView` — przekazuje `uczelnia` do `przygotuj_sekcje`; + `BuildSearch.post` obsługuje `charakter_formalny` (przez + `CharakterFormalnyQueryObject`, rozwiązuje po **nazwie**, nie pk). +- `bpp/templates/browse/autor.html` — 2 kolumny (Foundation grid w HTML): + lewa `cell large-4` (stała: zdjęcie→biogram→jednostka→historia + zatrudnienia→identyfikatory→metryki→stopnie→opis→cytowania→wyszukiwarka→ + raport), prawa `cell large-8` (pętla `sekcje_profilu`). Embed pełna szerokość + pod gridem. +- Partiale sekcji: `browse/autor_sekcje/*.html`. Współdzielone: + `_lista_prac.html` (stretched-link), `_wykres_lata.html` (liniowy>10 / + słupkowy≤10), `_historia_zatrudnienia.html`. +- `Autor.historia_zatrudnienia()` — `bpp/models/autor.py`. +- Admin: edytor układu (JSON textarea) w `UczelniaAdmin` (fieldset „Profil + autora (podstrona)"); `uklad_profilu` zniknęło z `AutorAdmin`. +- **§3.8 self-service**: `bpp/views/profil_edycja.py` (`ProfilEdycjaView`, + `AutorProfilForm`, `WymagajAutoraMixin`, `ProfilBiogramPodgladView`); URL-e + `bpp:profil-edycja`, `bpp:profil-biogram-podglad`; szablon + `bpp/profil_edycja.html` (live preview, debounce, CSRF); link „Edytuj swoją + stronę" na `bpp/profil_uzytkownika.html`. Autor edytuje TYLKO biogram+zdjęcie. +- Style: `bpp/static/scss/_autor-bem.scss` (2-kol, stretched-link + line-clamp, + wykres liniowy SVG, statystyki-link, historia). +- Testy: `src/bpp/tests/test_profil/` (test_uklad, test_models, test_widok, + test_lista_prac, test_wykresy, test_statystyki, test_historia, test_admin, + test_profil_edycja, test_biogram, test_obrazy) — 60 zielonych (świeży kontener). + +### 1.2. Lekcje / pułapki (żeby nie powtarzać) +- **CodeQL `js/xss-through-dom`**: zapis wartości z DOM do `window.location` + jest sinkiem (możliwy `javascript:`). Dlatego klik w pozycję listy prac + zrobiony jest jako **stretched-link** (pusta nakładka ``), bez JS. +- **§3.6**: `CharakterFormalnyQueryObject.value_from_web` rozwiązuje po + **`nazwa`** (MPTT z potomkami), NIE po pk. Formularz statystyk POST-uje + `autor=` + `charakter_formalny=`. +- **pytest-split / shardy**: dodanie testów zmienia podział na shardy i może + ujawnić testy zależne od danych bazowych, które poprzedzający test + transakcyjny wyczyścił (`flush`). Każdy test musi deklarować WSZYSTKIE swoje + zależności na danych referencyjnych (np. `typy_odpowiedzialnosci`). +- **CI**: realne gejty to `Build test-runner image` + `Tests (sharded)` (12 + shardów). Szybkie „success" <1 min = skip, nie dowód. Czytaj uważnie. + +## 2. Świadomie ODŁOŻONE (decyzja użytkownika 2026-06-19) +- **`make baseline-update`** — NIE robić teraz. Do zrobienia **raz, przy + scalaniu** (migracje 0444 + 0445; commit `baseline-sql/baseline.sql` + + `baseline.meta.json`). NIE w równoległych branchach. +- **Scalenie / domknięcie PR #385** — nie teraz. + +## 3. Co DALEJ (kandydaci na następną sesję; wybrać z użytkownikiem) + +Kolejność = sugerowany priorytet. Każdy punkt to osobny, niezależny kawałek. + +### 3.1. Wizualna weryfikacja strony (NISKI koszt, WYSOKA wartość) — REKOMENDOWANE NAJPIERW +Duża przebudowa UI poszła bez ani jednego spojrzenia w przeglądarkę. +- `uv run run-site run` (lub `--no-browser` w tle), wejść na `/bpp/autor//` + autora z publikacjami, zdjęciem, biogramem i historią zatrudnienia. +- Sprawdzić: 2 kolumny na desktopie i stackowanie na mobile; stretched-link + (klik w pozycję działa, link DOI w opisie też); line-clamp opisu; wykres + liniowy (>10 lat) vs słupkowy (≤10); klik w charakter → multiseek; embed pod + gridem; `/profil/edycja/` (upload zdjęcia, live preview biogramu). +- Skalibrować wartości wstępne: próg „10 lat" wykresu, liczba linii line-clamp + (teraz 3), szerokości kolumn. To było zaznaczone jako „do kalibracji + wizualnej". + +### 3.2. Edytor kafelkowy układu w `UczelniaAdmin` (drag-drop) +Teraz MVP = JSON w textarea (`uklad_profilu_autora`). Docelowo ładny UI: +lista sekcji z checkbox widoczności + select limitu + drag-drop kolejności, +serializacja do tego samego JSON-a. **Najpierw sprawdzić istniejący JS +sortowania w repo** (jest `sortable_field_name` w inline'ach adminów) zanim +dołożysz zależność. Reużyć `waliduj_uklad`/`KATALOG_SEKCJI` z `profil_autora.py`. + +### 3.3. Eksport zbiorczy publikacji autora (BibTeX + RIS) +- BibTeX: reużycie `src/bpp/export/bibtex.py` (`export_to_bibtex`, + `.to_bibtex()` na `.original`). Endpoint `/autor//eksport.bib`. +- **RIS — net-new** (nie istnieje). Endpoint `/autor//eksport.ris`. +- Świadomy limit/stream dla autorów z dużą liczbą prac. +- Sekcja `eksport` została USUNIĘTA z rejestru w rewizji — eksport wpiąć jako + przyciski (lewa kolumna lub osobny blok), nie jako sekcję prawej kolumny. + +### 3.4. Self-service: picker wyróżnionych prac (`WybranaPublikacjaAutora`) +Model + inline w adminie już są (Faza 1). Brakuje self-service: autor +dodaje/usuwa/sortuje wyróżnione prace (autocomplete) w `/profil/edycja/`. +Sekcja `wybrane_publikacje` w rejestrze jest domyślnie OFF — uczelnia może ją +włączyć w układzie. + +### 3.5. Drobny polish (opcjonalnie) +- `_lista_prac.html`: `aria-label` nakładki jest generyczny („Szczegóły + publikacji") — można wstawić tytuł pracy. +- `profil_edycja.html`: dodać help/skrót składni Markdown przy biogramie. +- Sprawdzić render `opis` vs `biogram` w lewej kolumnie (oba mogą być widoczne). + +## 4. Reguły wykonania (z CLAUDE.md — trzymać się) +- TDD: test → patrz jak failuje → implementuj. Commit po commicie. +- Max 88 znaków (ruff). Komentarze django `{# #}` jedno-liniowe. +- Ikony: frontend publiczny → Foundation-Icons; admin → emoji. +- NIE nadpisywać klas grid Foundation w SCSS — zmieniać klasy w HTML. +- NIE edytować istniejących migracji. Nowa = `0446+`. +- `pre-commit` bez argumentów; fixy ręcznie (NIE `ruff --fix` batch). diff --git a/docs/superpowers/specs/2026-06-19-profil-autora-rewizja-2col-HANDOFF.md b/docs/superpowers/specs/2026-06-19-profil-autora-rewizja-2col-HANDOFF.md new file mode 100644 index 000000000..6fe400197 --- /dev/null +++ b/docs/superpowers/specs/2026-06-19-profil-autora-rewizja-2col-HANDOFF.md @@ -0,0 +1,200 @@ +# Profil autora — REWIZJA (2 kolumny, układ per-Uczelnia) + HANDOFF + +Data: 2026-06-19 +Dotyczy: kontynuacji prac nad PR #385 (branch `feature/profil-autora`). +Spec bazowy: `docs/superpowers/specs/2026-06-18-profil-autora-i-podstrona-design.md`. + +> Ten dokument jest samowystarczalny — świeża sesja Claude'a ma z niego wznowić +> bez dostępu do poprzedniej rozmowy. + +## 0. Stan obecny (co JUŻ jest na branchu) + +- Worktree: `~/Programowanie/bpp-profil-autora`, branch `feature/profil-autora`, + PR **#385** → `dev`. **CI w pełni zielone** (lint, build test-runner, + 12 shardów Tests, vitest, baseline freshness, CodeQL). +- Remote push: SSH nie działa (brak klucza); pushuj przez HTTPS z gh: + `git push https://github.com/iplweb/bpp.git feature/profil-autora:feature/profil-autora` + (po `gh auth setup-git`). gh zalogowany jako `mpasternak` (token HTTPS). +- Testy: `PYTEST_TESTCONTAINERS_REUSE=1 uv run pytest src/bpp/tests/test_profil/`. + UWAGA: reused testcontainer bywa STALE (błąd `bpp_uczelnia.site_id NOT NULL` + przy fixture `uczelnia`). Gdy to wyskoczy → `make clean-testcontainers` i + odpal bez reuse (świeży kontener jest OK). +- Faza 1 dostarczyła (działa, otestowane 35+ testów w `src/bpp/tests/test_profil/`): + - Model `Autor`: `zdjecie` (ImageField), `biogram` + `biogram_format` (md/html), + `uklad_profilu` (JSONField — UWAGA: do PRZENIESIENIA na Uczelnię, patrz §2.1), + `cached_property biogram_html`. Model `WybranaPublikacjaAutora` (GenericFK). + Migracja `0444`. + - `bpp/util/biogram.py` `renderuj_biogram` + `bpp/util/text.py` + `safe_biogram_html` (nh3, bogatszy zestaw tagów, usuwa script/style, + rel=nofollow noopener). + - `bpp/util/obrazy.py` `przetworz_zdjecie_autora` (EXIF→crop→WebP 400×400). + - `bpp/profil_autora.py` — rejestr sekcji (`KATALOG_SEKCJI`, `TypSekcji`, + `KLUCZ_*`, `waliduj_uklad`, `rozwiaz_uklad`, `domyslny_uklad`). + - `bpp/profil_autora_dane.py` — buildery danych sekcji (`przygotuj_sekcje`). + - `bpp/views/browse.py` `AutorView` — render sekcji + `_raport_links`. + - `bpp/templates/browse/autor.html` + `browse/autor_sekcje/*.html`. + - `bpp/admin/autor.py` — fieldset profilu, `clean_zdjecie`, inline + `WybranaPublikacjaAutoraInline`. + - `bpp/static/scss/_autor-bem.scss` — style sekcji. + +## 1. Czego chce użytkownik (rewizja — wiadomość 2026-06-19) + +1. **Klik w całą pozycję pracy** (najlepsze/najnowsze/ostatnio edytowane) ma + prowadzić do szczegółów — NIE link `[szczegóły]` na końcu. +2. **Opisy bibliograficzne bywają dramatycznie długie** (dużo autorów) — trzeba + je rozsądnie skracać. +3. **Wykres „Publikacje w latach"**: dla >10 lat robi się za szeroki → powyżej + 10 lat wersja **liniowa**, do 10 lat **słupkowa**. +4. **Statystyki wg charakteru**: klik w charakter formalny ma **budować + wyszukiwanie** w formularzu (dany autor + ten charakter formalny). +5. **Układ 2-kolumnowy** strony autora: + - LEWA: klasyka — (zdjęcie+biogram na górze), aktualna jednostka, + historia zatrudnienia, wyszukiwarka prac, linki do raportów (+ pozostałe + bloki tożsamości: identyfikatory, metryki, stopnie, cytowania). + - PRAWA (od góry, domyślnie): Statystyki wg charakteru → wykres prac w latach + → (wykres PK) → (wykres IF, **tylko jeśli IF ≠ 0**) → współautorzy → + długi ogon „najlepsze prace" → najnowsze artykuły → najnowsze książki → + ostatnio edytowane. +6. **Edytor kafelkowy = admin-only, układ GLOBALNY per-Uczelnia** (system bywa + multi-uczelniany). Autor self-service edytuje TYLKO biogram + zdjęcie. +7. **Historia zatrudnienia** w jednostkach — sekcja w lewej kolumnie pod + aktualną jednostką (dane z `Autor_Jednostka`). + +## 2. Decyzje (zatwierdzone 2026-06-19) + +| # | Decyzja | +|---|---| +| Układ — zakres | **Globalny per-Uczelnia**. Render bierze `Uczelnia.objects.get_for_request(request)` i czyta jego układ. `Autor.uklad_profilu` — usunąć (override per-autor NIEpotrzebny). | +| „Najnowsze" listy | Zostają w prawej kolumnie, w długim ogonie pod „najlepszymi". | +| Zdjęcie/biogram | Na górze LEWEJ kolumny (wizytówka). | +| Historia zatrudnienia | TAK, sekcja w lewej kolumnie pod „aktualna jednostka". | +| Skracanie opisów | CSS line-clamp (~3 linie) + cała pozycja klikalna do szczegółów. | +| Próg wykresu | >10 lat → liniowy (SVG), ≤10 lat → słupkowy. | +| Klik w charakter | POST do `bpp:browse_build_search` z `autor` + charakter formalny. | + +## 3. Plan implementacji (konkretnie) + +### 3.1. Przeniesienie układu na Uczelnię (multi-uczelnia) + +- **Model**: dodać `Uczelnia.uklad_profilu_autora = JSONField(null=True, blank=True, + default=None)` (schemat jak dotychczasowy `uklad_profilu`: lista + `{"klucz","widoczna","limit"}` — ale tylko sekcje PRAWEJ kolumny, patrz §3.2). +- **Migracja 0445** (NIE edytować 0444 — reguła CLAUDE.md): `AddField` na + Uczelni + `RemoveField(Autor, "uklad_profilu")` (pole nieshipowane, więc + usunięcie czyste; zero danych produkcyjnych). +- **`rozwiaz_uklad`**: zmienić sygnaturę z `(autor)` na `(uczelnia)` — czyta + `uczelnia.uklad_profilu_autora` (lub `None`→default). Zaktualizować testy + `test_uklad.py` (stub `SimpleNamespace(uklad_profilu_autora=...)`). +- **`przygotuj_sekcje(autor, uczelnia, request)`** — układ z uczelni, dane z autora. +- **`AutorView`**: ma już `uczelnia = Uczelnia.objects.get_for_request(...)`; + przekazać do `przygotuj_sekcje`. +- **Admin**: usunąć `uklad_profilu` z `AutorForm`/fieldsetu Autora; dodać edytor + układu w adminie **Uczelni** (`UczelniaAdmin`). MVP: JSON w textarea + help. + (Kafelkowy drag-drop można dołożyć później — patrz §4.) + +### 3.2. Rejestr sekcji — tylko PRAWA kolumna + +Lewa kolumna jest STAŁA w szablonie (klasyka). Rejestr (`KATALOG_SEKCJI`) +obsługuje wyłącznie kafelki PRAWEJ kolumny. Usuń z rejestru: `wyszukiwarka`, +`biogram`, `eksport` (wyszukiwarka+biogram → lewa stała; eksport → Faza 2). +Zostają (domyślny porządek prawej kolumny): + +1. `statystyki_charakter` (ON) +2. `wykres_lata` (ON) — liczba prac/rok +3. `wykres_pk_lata` (ON) — suma `punkty_kbn`/rok ← NOWA +4. `wykres_if_lata` (ON, auto-hide gdy suma IF = 0) — suma `impact_factor`/rok ← NOWA +5. `wspolautorzy` (ON) +6. `najlepsze_pk` (ON) +7. `najlepsze_if` (ON) +8. `najnowsze_artykuly` (ON) +9. `najnowsze_zwarte` (ON) +10. `ostatnio_edytowane` (ON) +11. `dyscypliny` (OFF), `zrodla` (OFF), `punkty_lata` (OFF), `wybrane_publikacje` (OFF) + +Usuń `obowiazkowa` z `TypSekcji` (była tylko dla wyszukiwarki). Buildery +`_biogram`, `_wyszukiwarka`, `_eksport` z `profil_autora_dane.py` — usunąć. + +### 3.3. Szablon 2-kolumnowy (`browse/autor.html`) + +- Foundation grid (NIE nadpisywać klas grid w SCSS — zmiana w HTML): + `grid-x grid-margin-x` → `cell large-4` (lewa) + `cell large-8` (prawa); + na małych ekranach stackuje się automatycznie. +- Nagłówek (breadcrumb + H1 + funkcja + przyciski staff) — full-width nad gridem. +- LEWA `cell large-4` (kolejność): zdjęcie (awatar) → biogram → aktualna + jednostka → **historia zatrudnienia** → identyfikatory → metryki → stopnie → + cytowania → wyszukiwarka prac (`autor_sekcje/wyszukiwarka.html`) → linki + raportu → (embed-kod). Wszystko STAŁE w szablonie. +- PRAWA `cell large-8`: pętla `{% for s in sekcje_profilu %}{% include s.template ... %}{% endfor %}`. + +### 3.4. Listy prac — klik w całość + skracanie + +`browse/autor_sekcje/_lista_prac.html`: +- Usuń link `[szczegóły]`. Każda pozycja `
  • ` klikalna w całość → `data-href` + = `praca.get_absolute_url`; mały, delegowany JS: klik w `.autor-page__praca` + nawiguje do `data-href`, CHYBA że kliknięto wewnętrzny `` (np. DOI). Nie + zagnieżdżaj `` w `` (opis_bibliograficzny_cache zawiera własne linki). +- Skracanie: kontener opisu z CSS line-clamp (~3 linie, overflow hidden, + `text-overflow: ellipsis` / `-webkit-line-clamp`). Cała pozycja i tak klikalna + do pełnych szczegółów. + +### 3.5. Wykresy (liniowy/słupkowy) + +- Wspólny partial `browse/autor_sekcje/_wykres_lata.html`: dane = lista + `(rok, wartosc)` + `maks`. Jeśli `len(dane) > 10` → SVG `` (liniowy), + inaczej słupki (jak obecnie). Bezzależnościowo (czysty SVG/HTML). +- Buildery w `profil_autora_dane.py`: + - `_wykres_lata` (jest) — liczba prac/rok. + - `_wykres_pk_lata` (NOWY) — `prace_autora` grupuj po `rok`, suma `punkty_kbn`. + - `_wykres_if_lata` (NOWY) — suma `impact_factor`/rok; **return None gdy suma=0**. + Histogramy: pobierz pary `(id, rok, wartosc)` z `values_list` (DISTINCT z `id` + neutralizuje duplikaty join `autorzy`), sumuj w Pythonie. +- Sekcje `wykres_pk_lata`, `wykres_if_lata` w rejestrze + szablony korzystają + z `_wykres_lata.html` (przekaż `dane`, `maks`, `etykieta`). + +### 3.6. Statystyki wg charakteru — klikalne → wyszukiwarka + +- `statystyki_charakter.html`: każdy wiersz = mały `
    ` z `autor=` + + `charakter_formalny=` (lub przycisk-link). Builder musi zwrócić też + `charakter_formalny_id` (nie tylko nazwę) — zmień `_statystyki_charakter` na + `values_list("id","charakter_formalny__id","charakter_formalny__nazwa")`. +- **DO WERYFIKACJI**: `BuildSearch` (`browse.py` ~632-691) obecnie obsługuje + `autor`, `typy`, `jednostka`, `rok`, `suggested-title`. Trzeba dodać obsługę + `charakter_formalny` → zmapować na multiseek query object dla charakteru + formalnego (sprawdź `bpp/multiseek_registry.py` — czy jest CharakterFormalny + QueryObject; jeśli nie, użyć `TypRekorduObject`/`charakter`). To jedyny + fragment wymagający rozpoznania przed kodowaniem. + +### 3.7. Historia zatrudnienia (lewa kolumna) + +- Metoda `Autor.historia_zatrudnienia()` → `Autor_Jednostka.objects.filter( + autor=self).select_related("jednostka","funkcja").order_by("-rozpoczal_prace")`. + (Model `Autor_Jednostka` w `bpp/models/autor.py` ~545: pola `jednostka`, + `rozpoczal_prace`, `zakonczyl_prace`, `funkcja`, `podstawowe_miejsce_pracy`.) +- Partial w lewej kolumnie: lista „Jednostka — od–do (funkcja)". Pominąć wiersze + bez dat lub pokazać „obecnie" gdy brak `zakonczyl_prace`. + +### 3.8. Self-service autora (Faza 2, zawężona) + +Autor edytuje TYLKO biogram (MD/HTML + live preview) i zdjęcie (upload+podgląd). +Brak edytora układu po stronie autora. Gate: zalogowany + `request.user.autor`. +Widok w „Mój profil" (`bpp:profil-uzytkownika`). + +## 4. Otwarte / do decyzji później + +- Edytor kafelkowy (drag-drop) układu prawej kolumny w `UczelniaAdmin` — MVP to + JSON w textarea; ładny kafelkowy UI to osobne zadanie. +- Eksport zbiorczy BibTeX/RIS — Faza 2. +- Próg „10 lat" i liczba linii line-clamp — wartości wstępne, do kalibracji + wizualnej. + +## 5. Po implementacji + +- `make baseline-update` **przy scalaniu** (migracje 0444 + 0445) — commit + `baseline-sql/baseline.sql` + `baseline.meta.json`. NIE w branchu równolegle. +- `grunt build` po zmianach SCSS (skompilowane CSS jest poza gitem — kontrakt + build-time; commituj tylko źródła SCSS). +- ruff/ruff-format + djLint przez pre-commit; CI „Lint changed files" odpala + ruff-format na zmienionym zakresie z `--exit-non-zero-on-fix` → KAŻDY nowy + plik .py musi być pre-formatowany (`uv run ruff format `), łącznie z + migracjami Django (mają długie linie). diff --git a/docs/superpowers/specs/2026-07-07-radon-osiagniecia-autora-design.md b/docs/superpowers/specs/2026-07-07-radon-osiagniecia-autora-design.md new file mode 100644 index 000000000..801e6cd88 --- /dev/null +++ b/docs/superpowers/specs/2026-07-07-radon-osiagniecia-autora-design.md @@ -0,0 +1,224 @@ +# Osiągnięcia autora z RAD-on OpenData (client-side, po ORCID) + +**Data:** 2026-07-07 +**Gałąź:** `feat/radon-profil-autora` (na bazie `feature/profil-autora`, PR #385) +**Status:** spec zaakceptowany do implementacji + +## Cel + +Na podstronie autora (`/autor//`) dla naukowców posiadających ORCID +dodać sekcję z ich osiągnięciami pobranymi **na żywo z RAD-on OpenData** — +**po stronie klienta** (fetch z przeglądarki), z podpisem drobnymi literami +„informacje pobrane z RAD-on". + +Pokazujemy to, czego BPP nie ma we własnej bazie: **stopnie/tytuły naukowe, +zatrudnienie, projekty naukowe (z kwotami), patenty i prawa ochronne, +osiągnięcia artystyczne (z nagrodami)**. **Publikacji NIE pobieramy** — BPP ma +je we własnej bazie. + +Kod odpytujący RAD-on ma być **wydzielonym, przenośnym modułem JS** (bez +zależności od BPP), tak by dało się go wyjąć i użyć gdzie indziej. + +## Ustalenia badawcze (przetestowane na żywo 2026-07-07) + +Reverse-engineering + testy żywego API. **Cała rodzina `/opendata/*` ma CORS +otwarty** (preflight/GET/POST odbijają `Origin` → fetch z przeglądarki działa; +zweryfikowane na `scientist/search`, `polon/projects`, `polon/products`, +`polon/artisticAchievements`, `polon/publications`). + +Wszystkie endpointy: baza `https://radon.nauka.gov.pl/opendata`, odpowiedź +`{results[], pagination{maxCount, token}, version}`, paginacja kursorem +`token` (`null`/pominięty na starcie). + +### A. `scientist` (dane zintegrowane) — CV: stopnie/tytuły/zatrudnienie + +- **`POST /opendata/scientist/search`**, ciało: + ```json + {"resultNumbers": 10, "token": null, "body": {"firstName": "...", "lastName": "..."}} + ``` + `token` MUSI być `null`/pominięty na starcie (`""` → `400 Malformed token`). + Kryteria (`body`): `firstName`, `lastName`, `uid`, markery. **Bez filtra + `orcid`.** +- Rekord: `personalData{orcid, id(uid POL-on 40-hex), firstName, middleName, + lastName}`, `academicDegrees[]` (Doktor/Doktor habilitowany + `grantingYear` + + `grantingInstitutionName` + `degreeClassification`), `academicTitles[]` + (profesura + rok), `professionalTitles[]` (magisterium + rok + uczelnia), + `employments[]`, `calculatedEduLevel`, `dataSources`. +- **Wynik zawiera `personalData.orcid`** → dopasowanie po nazwisku, weryfikacja + po ORCID. + +### B. `polon/projects` — PROJEKTY NAUKOWE (filtr po kierowniku) + +- **`GET /opendata/polon/projects`** z parametrami: + `projectManagerFirstName`, `projectManagerLastName`, `disciplineName`, + `disciplineCode`, `entityShowingAchievementsName`, `projectStartDate`, + `projectEndDate`, `resultNumbers`, `token`. +- Rekord: `projectTitlePl`/`projectTitleEn`, `acronym`, `totalFunds`, + `receivedFunds`, `nationalFunds`, `foreignFunds`, `projectStartDate`, + `projectEndDate`, `projectGrantDate`, `projectClassification`, + `financedCompetition`, `financingInstitutions[]`, `implementingInstitutions[]`, + `disciplines[]`, `dataSource`, oraz **`projectManagers[]`** z polami + `firstName`, `middleName`, `lastName`, **`ORCID`**, `kindManager`, + `institutionName`, `startDate`, `endDate`. +- **Weryfikacja:** `projectManagers[].ORCID == autor.orcid` (potwierdzone na + żywo: Kowalczewski → 3 projekty, manager ORCID = `0000-0002-0153-4624`, + kwoty w `totalFunds`). + +### C. `polon/products` — PATENTY I PRAWA OCHRONNE (filtr po wynalazcy) + +- **`GET /opendata/polon/products`**: + `inventorFirstName`, `inventorLastName`, `institutionName`, + `productTitle`, `protectionTypeCode`, `publicationDateFrom/To`, + `resultNumbers`, `token`. +- Rekord: `productTitles[]`, `protectionType`, `protectionTitle`, + `publicationNumber`, `publicationDate`, `applicationDate`, + `grantingInstitutionName`, `productDescription`, `applicants[]`, oraz + **`inventors[]`** → `persons[]` z `firstName`, `lastName`, + **`relatedOrcid`** (bywa `null`). +- **Weryfikacja:** `inventors[].persons[].relatedOrcid == autor.orcid`; gdy + `relatedOrcid` puste — dopasowanie tylko po nazwisku (słabsze; oznaczamy jako + „niezweryfikowane po ORCID" wewnętrznie, patrz reguła niżej). + +### D. `polon/artisticAchievements` — OSIĄGNIĘCIA ARTYSTYCZNE (filtr po autorze) + +- **`GET /opendata/polon/artisticAchievements`**: + `authorFirstName`, `authorLastName`, `institutionName`, `title`, + `achievementKindCode`, `implementationYearFrom/To`, `resultNumbers`, `token`. +- Rekord: `title`, `discipline`, `achievementKind`, `achievementType`, + `implementationYear`, `firstPublicationYear`, `publisherName`, + `achievementRange`, **`awards[]`** (`competitionName`, `awardYear`, + `awardingInstitution`), oraz **`authors[]`** (`AuthorData`) z `firstName`, + `lastName`, **`orcid`**. +- **Weryfikacja:** `authors[].orcid == autor.orcid`. (Domena akademii sztuk — + dla większości autorów `maxCount=0`; sekcja typu chowa się pusta.) + +### Czego świadomie NIE używamy + +- **Publikacje** (`polon/publications`, filtr `orcidId` istnieje) — **BPP ma + publikacje we własnej bazie**. Poza zakresem. +- **`polon/employees`** — brak ORCID, odrzucone na rzecz `scientist`. +- **Ludzie Nauki** (`ludzie.nauka.gov.pl`) — CORS zamknięty, OAuth, + `profileId` nie z ORCID. Poza zakresem. +- **Usługi zintegrowane `project`/`publication`/`product`** — kryteria bez + osoby; używamy per-osobowych usług POL-on (B/C/D powyżej). + +## Zakres + +### Wchodzi + +1. **Przenośny moduł JS „odpytywacz RAD-on"** (`radon-client`), bez zależności + od BPP, natywny `fetch`, konfigurowalny `baseUrl` (domyślnie + `https://radon.nauka.gov.pl/opendata`), z metodami: + - `searchScientist({firstName, lastName})` → `Scientist[]` (POST + `scientist/search`). + - `fetchProjects({firstName, lastName})` → `Project[]` + (GET `polon/projects`). + - `fetchPatents({firstName, lastName})` → `Patent[]` + (GET `polon/products`). + - `fetchArtisticAchievements({firstName, lastName})` → `Achievement[]` + (GET `polon/artisticAchievements`). + - Helper `orcidMatches(a, b)` — normalizuje (usuwa URL/spacje/myślniki, + case-insensitive) i porównuje. + - Selektory: `pickScientistByOrcid`, `filterProjectsByOrcid` (po + `projectManagers[].ORCID`), `filterPatentsByOrcid` (po + `inventors[].persons[].relatedOrcid`), `filterAchievementsByOrcid` (po + `authors[].orcid`). + - Normalizatory `extract*` → płaskie obiekty do renderu (patrz „Pola + wyświetlane"). + - Zero zależności zewnętrznych; sprawdzone na NPM — gotowego klienta + RAD-on/POL-on nie ma (repo OPI to notebooki R/Python). +2. **Warstwa integracji BPP** (cienki, osobny plik JS) — czyta `data-*` + (imię, nazwisko, orcid) z kontenera sekcji, woła powyższe metody + równolegle (`Promise.allSettled`), filtruje po ORCID, renderuje pod-bloki, + dokleja podpis „informacje pobrane z RAD-on". +3. **Sekcja w rejestrze profilu** (`bpp.profil_autora`): nowy klucz + `KLUCZ_RADON = "radon_osiagniecia"`, `TypSekcji(..., template_only=True)`, + renderowana tylko gdy `autor.orcid`. Kolumna domyślna: LEWA. +4. **Partial** `autor_sekcje/radon_osiagniecia.html`: kontener z + `data-orcid`/`data-imie`/`data-nazwisko`, stan „ładowanie…", cztery + pod-kontenery (CV / projekty / patenty / osiągnięcia artystyczne), podpis. +5. **Degradacja bez błędów, per pod-blok:** każdy typ danych ładuje się + niezależnie; brak sieci / 0 wyników / brak ORCID-matcha → dany pod-blok się + chowa; gdy wszystkie puste → cała sekcja się chowa. + +### Pola wyświetlane (per typ) + +- **CV (scientist):** stopnie „Doktor 2016, UP Poznań", „Doktor habilitowany + 2023", tytuł profesorski (rok), zatrudnienie (instytucja + od), dyscypliny. +- **Projekty:** tytuł PL, program/konkurs (`financedCompetition`), + kwota (`totalFunds`), lata (`projectStartDate`–`projectEndDate`), + instytucja finansująca, rola (`projectManagers[].kindManager`). +- **Patenty:** tytuł (`productTitles`), typ ochrony (`protectionType`), + nr i data publikacji, instytucja udzielająca. +- **Osiągnięcia artystyczne:** tytuł, rodzaj, rok, nagrody (`awards[]`: + konkurs + rok). + +### Świadomie poza zakresem + +Publikacje; Ludzie Nauki; proxy/OAuth; pola/migracje na modelu; cache po +stronie serwera; rozstrzyganie homonimów inne niż po ORCID. + +## Model danych + +**Brak zmian modelu, brak migracji.** Używamy `Autor.orcid` + imię/nazwisko. +Nic nie zapisujemy — dane ulotne, pobierane na żywo w przeglądarce. + +## Reguła dopasowania po ORCID + +1. Normalizacja ORCID po obu stronach do 16 znaków (cyfry + `X`), porównanie + case-insensitive. +2. **scientist / projekty / osiągnięcia artystyczne:** pokazujemy wyłącznie + rekordy ze zgodnym ORCID (`personalData.orcid` / `projectManagers[].ORCID` + / `authors[].orcid`). Brak zgodnego → pod-blok pusty → schowany. +3. **patenty:** rekord ze zgodnym `relatedOrcid` pokazujemy zawsze; rekord z + `relatedOrcid == null` (dane POL-on niekompletne) pokazujemy tylko gdy + nazwisko+imię są jednoznaczne dla tego autora — inaczej pomijamy. (Patenty + bez ORCID to jedyny słabszy przypadek; świadomie ostrożni.) + +## Bezpieczeństwo / prywatność + +- Wywołania do publicznego, rządowego API RAD-on (dane jawne POL-on/PBN). +- ORCID + imię/nazwisko są już publiczne na podstronie — brak nowego wycieku. +- Brak sekretów/tokenów w JS. Brak `credentials` w `fetch`. +- Sekcja tylko gdy `autor.orcid` — nie odpytujemy RAD-on bez potrzeby. +- Render przez `textContent`/tworzenie węzłów, **nie** `innerHTML` z surowych + pól API → zero XSS z odpowiedzi RAD-on. + +## Obsługa błędów (żadnych cichych połknięć) + +- Każde zapytanie w `Promise.allSettled`; odrzucone/`!ok`/timeout → + `console.debug("[radon] ", err)` + schowanie danego pod-bloku. + Nie `throw` w górę (nie wywalamy strony), ale zawsze logujemy powód. +- Nieoczekiwany kształt odpowiedzi → traktowany jak brak wyniku (log + chowaj). + +## Testy + +**JS:** +- Każda metoda `fetch*`: buduje poprawny URL/ciało, parsuje `results`, + obsługuje pustkę i błąd sieci (mock `fetch`). +- `orcidMatches`: różne formaty (URL, myślniki, spacje), case, `null`. +- Selektory `filter*ByOrcid`: rozróżniają dwie osoby o tym samym nazwisku po + ORCID; projekty po `projectManagers[].ORCID`; patenty po + `inventors[].persons[].relatedOrcid`; osiągnięcia po `authors[].orcid`. +- `extract*`: mapują pola do renderu; odporne na brakujące/`null`. +- Degradacja: część zapytań odrzucona → pozostałe pod-bloki renderują, + odrzucone chowają się, brak wyjątku globalnego. + +**Python (pytest):** +- Render sekcji: `autor.orcid` ustawiony → kontener z poprawnymi `data-*`; + pusty ORCID → brak sekcji. +- Sekcja w `KATALOG_SEKCJI`, przechodzi `waliduj_uklad`/`rozwiaz_uklad` + (forward-compat: dokleja się bez migracji danych). + +**Bez E2E odpytującego żywy RAD-on** (flaky/sieć zewnętrzna). Kontrakt API +udokumentowany w tym specu; logika na mockach. + +## Ryzyka + +- RAD-on zmieni API/kryteria/CORS → dany pod-blok degraduje do „schowany". + Kontrakt spisany tu. +- Autor bez rekordów w RAD-on → pod-bloki puste → schowane. Oczekiwane. +- Rozjazd imię/nazwisko BPP vs POL-on → brak dopasowania. Akceptowalne; + ORCID-match jest twardym warunkiem poprawności (poza opisanym wyjątkiem + patentów bez ORCID). +- Patenty bez `relatedOrcid` → ostrożna reguła nazwiskowa; ryzyko rzadkie. diff --git a/docs/uzytkownik/przegladanie-i-wyszukiwanie.md b/docs/uzytkownik/przegladanie-i-wyszukiwanie.md index 0b1f5aba8..7d1e59345 100644 --- a/docs/uzytkownik/przegladanie-i-wyszukiwanie.md +++ b/docs/uzytkownik/przegladanie-i-wyszukiwanie.md @@ -66,7 +66,7 @@ od listy do listy aż do konkretnego rekordu. Dostępne są widoki: w źródło zobaczysz opublikowane w nim prace. - **Wg roku** — prace pogrupowane według roku wydania. -## Strona autora („raport autora") +## Strona autora („raport autora") {#strona-autora} Po wejściu w nazwisko na liście autorów otwiera się **strona autora** — zestawienie wszystkich jego publikacji w bazie wraz z podstawowymi danymi @@ -74,6 +74,77 @@ zestawienie wszystkich jego publikacji w bazie wraz z podstawowymi danymi To najprostszy sposób, by zobaczyć dorobek jednej osoby bez budowania zapytania. +Strona autora ma **układ dwukolumnowy**. Na szerokim ekranie obie kolumny +widoczne są obok siebie; na wąskich ekranach (telefon, tablet) kolumny +układają się jedna pod drugą — najpierw wizytówka, potem statystyki i listy +prac. + +!!! note "Zrzut ekranu" + W tym miejscu można w przyszłości umieścić zrzut ekranu przedstawiający + typowy układ dwukolumnowej strony autora. + +### Lewa kolumna — wizytówka autora + +Lewa kolumna to stała wizytówka osoby. Zawiera (zależnie od uzupełnienia +danych): + +- **Zdjęcie autora** — klikalne; kliknięcie powiększa fotografię w osobnym + okienku. +- **Biogram** — krótka nota o autorze. +- **Aktualna jednostka** oraz **historia zatrudnienia** — lista jednostek, + w których autor pracował, wraz z okresami i pełnioną funkcją. +- **Dyscypliny naukowe** przypisane autorowi. +- **Identyfikatory zewnętrzne** — m.in. ORCID, PBN UID/ID, odnośnik + „Naukowiec z POL-on" oraz wewnętrzny identyfikator BPP. +- **Metryki ewaluacyjne**, **stopnie naukowe** (doktorat, habilitacja) + oraz **liczba cytowań** (o ile uczelnia pobiera takie dane). +- Formularz **„Wyszukaj publikacje autora"** — szybkie zawężenie listy prac + po fragmencie tytułu. +- Odnośnik do **raportu autora** oraz **przyciski eksportu (BibTeX / RIS)** — + patrz [Eksport publikacji autora](#eksport-publikacji-autora-bibtex-i-ris). + +### Prawa kolumna — statystyki i zestawienia + +Prawa kolumna gromadzi statystyki i zestawienia dorobku. To, **które sekcje +są widoczne i w jakiej kolejności**, ustala uczelnia (patrz +[instrukcja administratora — układ podstrony autora](../administrator/ogolna.md#układ-podstrony-autora-profil-autora)). +Do dyspozycji są m.in.: + +- **statystyki wg charakteru formalnego** (np. ile artykułów, ile rozdziałów), +- **wykresy roczne** — liczba publikacji, suma punktów MNiSW oraz suma + Impact Factor; domyślnie obejmują ostatnie 10 lat, +- **najczęstsi współautorzy**, +- **najlepsze prace** wg punktacji oraz wg Impact Factor, +- **najnowsze artykuły** oraz **najnowsze książki i rozdziały**, +- **ostatnio edytowane** rekordy, +- a jeśli uczelnia włączyła tę sekcję — **wyróżnione publikacje** wskazane + przez samego autora. + +### Interakcje na stronie autora + +- Kliknięcie w pozycję na liście prac prowadzi do **szczegółów publikacji**. +- Kliknięcie w **nazwę charakteru formalnego** w tabeli statystyk otwiera + wyszukiwarkę precyzyjną (multiseek) z gotowym zapytaniem „ten autor + + ten charakter" — od razu zobaczysz wszystkie prace danego rodzaju. + +## Eksport publikacji autora (BibTeX i RIS) {#eksport-publikacji-autora-bibtex-i-ris} + +W lewej kolumnie strony autora znajdują się przyciski **„BibTeX"** i +**„RIS"**. Pobierają one plik zawierający **wszystkie publikacje autora** +(odpowiednio w formacie `.bib` oraz `.ris`), gotowy do zaimportowania +w menedżerach bibliografii — np. Zotero, Mendeley, EndNote, a w przypadku +BibTeX-a także w środowisku LaTeX. + +Eksport jest dostępny publicznie, bez logowania, pod adresami: + +- `/bpp/autor//eksport.bib` — format BibTeX, +- `/bpp/autor//eksport.ris` — format RIS, + +gdzie `` to identyfikator autora. + +!!! note + Eksport obejmuje do 5000 prac jednego autora. + ## Ranking autorów Menu **raporty → ranking autorów** otwiera formularz, w którym wybierasz diff --git a/package.json b/package.json index 841a23f35..ee2368421 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ }, "dependencies": { "3d-force-graph": "^1.80.0", + "@iplweb/radon-opendata": "^0.1.1", "basic-ftp": "5.3.1", "cytoscape": "^3.34.0", "cytoscape-fcose": "^2.2.0", diff --git a/src/bpp/admin/autor.py b/src/bpp/admin/autor.py index 4fc86a064..c5b218b85 100644 --- a/src/bpp/admin/autor.py +++ b/src/bpp/admin/autor.py @@ -1,6 +1,7 @@ from dal import autocomplete from django import forms from django.contrib import admin +from django.core.files.uploadedfile import UploadedFile from dynamic_admin_columns.mixins import DynamicColumnsMixin from bpp.admin.helpers.djangoql import BppDjangoQLSearchMixin @@ -14,7 +15,9 @@ Autor_Jednostka, Dyscyplina_Naukowa, Jednostka, + WybranaPublikacjaAutora, ) +from ..util.obrazy import MAKS_ROZMIAR_PLIKU_ZDJECIA, przetworz_zdjecie_autora from .actions import ustaw_pokazuj_false, ustaw_pokazuj_true from .core import BaseBppAdminMixin from .filters import ( @@ -195,6 +198,9 @@ class Meta: "zmarl", "opis", "pokazuj_opis", + "zdjecie", + "biogram", + "biogram_format", "poprzednie_nazwiska", "pokazuj_poprzednie_nazwiska", "orcid", @@ -206,6 +212,28 @@ class Meta: ] widgets = {"imiona": CHARMAP_SINGLE_LINE, "nazwisko": CHARMAP_SINGLE_LINE} + def clean_zdjecie(self): + """Waliduj rozmiar i przeskaluj świeżo wgrane zdjęcie do kwadratu WebP. + + Istniejący (niezmieniony) plik przechodzi bez przetwarzania. + """ + plik = self.cleaned_data.get("zdjecie") + if not isinstance(plik, UploadedFile): + return plik + if plik.size > MAKS_ROZMIAR_PLIKU_ZDJECIA: + raise forms.ValidationError("Maksymalny rozmiar pliku zdjęcia to 5 MB.") + return przetworz_zdjecie_autora(plik, nazwa=plik.name) + + +class WybranaPublikacjaAutoraInline(admin.TabularInline): + # Relacja do Autora to zwykły FK `autor`; content_type+object_id wskazują + # polimorficzną publikację (GenericForeignKey). W Fazie 1 edycja ręczna; + # przyjazny picker dostarcza self-service edytor z Fazy 2. + model = WybranaPublikacjaAutora + fk_name = "autor" + extra = 0 + fields = ["content_type", "object_id", "kolejnosc"] + class AutorAdmin( SiteFilteredAdminMixin, @@ -296,6 +324,7 @@ def has_delete_permission(self, request, obj=None): Autor_DyscyplinaInline, Autor_AbsencjaInline, IloscUdzialowDlaAutoraZaRokInline, + WybranaPublikacjaAutoraInline, ] list_filter = [ JednostkaFilter, @@ -360,6 +389,19 @@ def has_delete_permission(self, request, obj=None): ), }, ), + ( + "Profil na podstronie autora", + { + "classes": ("grp-collapse grp-closed",), + "fields": ( + "zdjecie", + "biogram", + "biogram_format", + "opis", + "pokazuj_opis", + ), + }, + ), ADNOTACJE_FIELDSET, ) diff --git a/src/bpp/admin/uczelnia.py b/src/bpp/admin/uczelnia.py index 3c05685a4..928d6f8a5 100644 --- a/src/bpp/admin/uczelnia.py +++ b/src/bpp/admin/uczelnia.py @@ -1,3 +1,5 @@ +import json + from django import forms from django.conf import settings from django.contrib import admin, messages @@ -8,6 +10,7 @@ from pbn_api.exceptions import PraceSerwisoweException from ..models import Uczelnia, Ukryj_Status_Korekty, Wydzial +from ..profil_autora import waliduj_uklad # Uczelnia from .core import BaseBppAdminMixin, RestrictDeletionToAdministracjaGroupMixin @@ -15,6 +18,7 @@ from .helpers.fieldsets import ADNOTACJE_FIELDSET from .helpers.mixins import ZapiszZAdnotacjaMixin from .helpers.site_filtered import SiteFilteredAdminMixin +from .widgets.uklad_profilu import EdytorUkladuWidget class WydzialInlineForm(forms.ModelForm): @@ -72,10 +76,11 @@ class UczelniaAdminForm(forms.ModelForm): class Meta: model = Uczelnia - # Tylko pole, które tu nadpisujemy — admin i tak regeneruje pełną + # Tylko pola, które tu nadpisujemy — admin i tak regeneruje pełną # listę pól z fieldsets przez modelform_factory, ten Meta.fields jest # wtedy przesłaniany. Wystarcza do samodzielnego instancjonowania formy. - fields = ["theme_name"] + fields = ["theme_name", "uklad_profilu_autora"] + widgets = {"uklad_profilu_autora": EdytorUkladuWidget} def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -93,6 +98,23 @@ def __init__(self, *args, **kwargs): "adresem — nie istnieje „uczelnia domyślna”." ) + def clean_uklad_profilu_autora(self): + """Sanityzuj zserializowaną wartość edytora do poprawnej listy. + + Widget posta JSON-string (lub listę gdy JS nie wystartował); + ``waliduj_uklad`` zrzuca nieznane/zduplikowane klucze i koryguje + widoczność/limit. Puste = ``None`` (``rozwiaz_uklad`` to toleruje). + """ + wartosc = self.cleaned_data.get("uklad_profilu_autora") + if not wartosc: + return None + if isinstance(wartosc, str): + try: + wartosc = json.loads(wartosc) + except (ValueError, TypeError): + return None + return waliduj_uklad(wartosc) + class UczelniaAdmin( SiteFilteredAdminMixin, @@ -200,6 +222,20 @@ def get_queryset(self, request): ), }, ), + ( + "Profil autora (podstrona)", + { + "classes": ("grp-collapse grp-closed",), + "description": ( + "Układ OBU kolumn podstrony autora (globalny dla uczelni). " + "Przeciągnij kafelki, aby zmienić kolejność LUB przenieść " + "sekcję między kolumnami (lewa ⇄ prawa); odznacz, by ukryć; " + "dla list ustaw limit pozycji. Szerokość lewej kolumny " + "(prawa dopełnia do 12) ustawisz osobnym polem." + ), + "fields": ("szerokosc_lewej_kolumny", "uklad_profilu_autora"), + }, + ), ( "Wydruki", { diff --git a/src/bpp/admin/widgets/__init__.py b/src/bpp/admin/widgets/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/src/bpp/admin/widgets/uklad_profilu.py b/src/bpp/admin/widgets/uklad_profilu.py new file mode 100644 index 000000000..12cabfd8a --- /dev/null +++ b/src/bpp/admin/widgets/uklad_profilu.py @@ -0,0 +1,130 @@ +"""Kafelkowy (drag-drop) edytor układu profilu autora dla ``UczelniaAdmin``. + +Zastępuje surowy ``