Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
309 changes: 309 additions & 0 deletions docs/superpowers/specs/2026-06-18-profil-autora-i-podstrona-design.md

Large diffs are not rendered by default.

44 changes: 44 additions & 0 deletions src/bpp/admin/autor.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 (
Expand Down Expand Up @@ -177,6 +180,10 @@ class Meta:
"zmarl",
"opis",
"pokazuj_opis",
"zdjecie",
"biogram",
"biogram_format",
"uklad_profilu",
"poprzednie_nazwiska",
"pokazuj_poprzednie_nazwiska",
"orcid",
Expand All @@ -188,6 +195,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(
BppDjangoQLSearchMixin,
Expand Down Expand Up @@ -249,6 +278,7 @@ class AutorAdmin(
Autor_DyscyplinaInline,
Autor_AbsencjaInline,
IloscUdzialowDlaAutoraZaRokInline,
WybranaPublikacjaAutoraInline,
]
list_filter = [
JednostkaFilter,
Expand Down Expand Up @@ -313,6 +343,20 @@ class AutorAdmin(
),
},
),
(
"Profil na podstronie autora",
{
"classes": ("grp-collapse grp-closed",),
"fields": (
"zdjecie",
"biogram",
"biogram_format",
"opis",
"pokazuj_opis",
"uklad_profilu",
),
},
),
ADNOTACJE_FIELDSET,
)

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Generated by Django 5.2.15 on 2026-06-18 20:14

import django.db.models.deletion
from django.db import migrations, models


class Migration(migrations.Migration):
dependencies = [
("bpp", "0443_drop_pl_PL_collation"),
("contenttypes", "0002_remove_content_type_name"),
]

operations = [
migrations.AddField(
model_name="autor",
name="biogram",
field=models.TextField(
blank=True,
default="",
help_text="Notka biograficzna pokazywana na podstronie autora.",
verbose_name="Biogram",
),
),
migrations.AddField(
model_name="autor",
name="biogram_format",
field=models.CharField(
choices=[("md", "Markdown"), ("html", "HTML")],
default="md",
max_length=4,
verbose_name="Format biogramu",
),
),
migrations.AddField(
model_name="autor",
name="uklad_profilu",
field=models.JSONField(
blank=True,
default=None,
help_text="Kolejność, widoczność i limity sekcji podstrony autora. Puste = układ domyślny.",
null=True,
verbose_name="Układ profilu",
),
),
migrations.AddField(
model_name="autor",
name="zdjecie",
field=models.ImageField(
blank=True,
help_text="Zdjęcie profilowe autora. Przy zapisie przez formularz jest przeskalowane do kwadratu.",
null=True,
upload_to="autor_zdjecia",
verbose_name="Zdjęcie",
),
),
migrations.CreateModel(
name="WybranaPublikacjaAutora",
fields=[
(
"id",
models.AutoField(
auto_created=True,
primary_key=True,
serialize=False,
verbose_name="ID",
),
),
("object_id", models.PositiveIntegerField()),
(
"kolejnosc",
models.PositiveIntegerField(default=0, verbose_name="Kolejność"),
),
(
"autor",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
related_name="wybrane_publikacje",
to="bpp.autor",
),
),
(
"content_type",
models.ForeignKey(
on_delete=django.db.models.deletion.CASCADE,
to="contenttypes.contenttype",
),
),
],
options={
"verbose_name": "wybrana publikacja autora",
"verbose_name_plural": "wybrane publikacje autora",
"ordering": ("autor", "kolejnosc"),
"unique_together": {("autor", "content_type", "object_id")},
},
),
]
1 change: 1 addition & 0 deletions src/bpp/models/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Zaimportujmy wszystko
from .abstract import * # noqa
from .autor import * # noqa
from .wybrana_publikacja import * # noqa
from .cache import * # noqa
from .dyscyplina_naukowa import * # noqa
from .grant import * # noqa
Expand Down
38 changes: 38 additions & 0 deletions src/bpp/models/autor.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
from django.db.models import CASCADE, SET_NULL, Count, Q, Sum, UniqueConstraint
from django.urls.base import reverse
from django.utils import timezone
from django.utils.functional import cached_property
from tinymce.models import HTMLField

from bpp import const
from bpp.core import zbieraj_sloty
from bpp.models import LinkDoPBNMixin, ModelZAdnotacjami, ModelZNazwa, NazwaISkrot
from bpp.models.abstract import ModelZPBN_ID
from bpp.util import FulltextSearchMixin
from bpp.util.biogram import FORMAT_MARKDOWN, FORMATY_BIOGRAMU


class Tytul(NazwaISkrot):
Expand Down Expand Up @@ -130,6 +132,35 @@ class Autor(LinkDoPBNMixin, ModelZAdnotacjami, ModelZPBN_ID):
pokazuj_opis = models.BooleanField(
default=False, help_text="""Czy pokazywać tekst z pola 'Opis' na stronie?"""
)

zdjecie = models.ImageField(
"Zdjęcie",
upload_to="autor_zdjecia",
blank=True,
null=True,
help_text="Zdjęcie profilowe autora. Przy zapisie przez formularz "
"jest przeskalowane do kwadratu.",
)
biogram = models.TextField(
"Biogram",
blank=True,
default="",
help_text="Notka biograficzna pokazywana na podstronie autora.",
)
biogram_format = models.CharField(
"Format biogramu",
max_length=4,
choices=FORMATY_BIOGRAMU,
default=FORMAT_MARKDOWN,
)
uklad_profilu = models.JSONField(
"Układ profilu",
blank=True,
null=True,
default=None,
help_text="Kolejność, widoczność i limity sekcji podstrony autora. "
"Puste = układ domyślny.",
)
poprzednie_nazwiska = models.CharField(
max_length=1024,
blank=True,
Expand Down Expand Up @@ -208,6 +239,13 @@ class Autor(LinkDoPBNMixin, ModelZAdnotacjami, ModelZPBN_ID):
def get_absolute_url(self):
return reverse("bpp:browse_autor", args=(self.slug,))

@cached_property
def biogram_html(self):
"""Bezpieczny HTML biogramu (Markdown/HTML wg ``biogram_format``)."""
from bpp.util.biogram import renderuj_biogram

return renderuj_biogram(self.biogram, self.biogram_format)

def czy_pokazywac_siec_powiazan(self, uczelnia):
"""Efektywne ustawienie "pokazuj sieć powiązań" dla tego autora.

Expand Down
34 changes: 34 additions & 0 deletions src/bpp/models/wybrana_publikacja.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
"""Wybrane (wyróżnione) publikacje autora — ręcznie wskazane prace.

Publikacje w BPP są polimorficzne (Wydawnictwo_Zwarte / Wydawnictwo_Ciagle /
Patent / Praca_*), więc wskazujemy je przez GenericForeignKey
(``content_type`` + ``object_id``) — tak samo jak identyfikuje je cache
``Rekord``.
"""

from django.contrib.contenttypes.fields import GenericForeignKey
from django.contrib.contenttypes.models import ContentType
from django.db import models
from django.db.models import CASCADE

from bpp.models.autor import Autor

__all__ = ["WybranaPublikacjaAutora"]


class WybranaPublikacjaAutora(models.Model):
autor = models.ForeignKey(Autor, CASCADE, related_name="wybrane_publikacje")
content_type = models.ForeignKey(ContentType, CASCADE)
object_id = models.PositiveIntegerField()
publikacja = GenericForeignKey("content_type", "object_id")
kolejnosc = models.PositiveIntegerField("Kolejność", default=0)

class Meta:
app_label = "bpp"
verbose_name = "wybrana publikacja autora"
verbose_name_plural = "wybrane publikacje autora"
ordering = ("autor", "kolejnosc")
unique_together = [("autor", "content_type", "object_id")]

def __str__(self):
return f"{self.autor}: {self.publikacja}"
Loading
Loading