Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
d531583
docs(spec): opis bibliograficzny z dysku zamiast dbtemplates (#329)
mpasternak Jul 8, 2026
49b9191
docs(spec): rewizja po recenzji Fable — denorm rebuild + konsumenci (…
mpasternak Jul 8, 2026
407ced6
docs(spec): druga runda Fable — guard dysk-existence + drobne (#329)
mpasternak Jul 8, 2026
b30833a
docs(plan): plan implementacji opis-z-dysku (8 tasków, TDD) (#329)
mpasternak Jul 8, 2026
c3268bf
feat(dbtemplates): helper disk_template_source — źródło z dysku bez d…
mpasternak Jul 8, 2026
4bd745a
fix(dbtemplates): compare_dbtemplates czytał DB zamiast dysku (#329)
mpasternak Jul 8, 2026
6eb71fb
feat(dbtemplates): usun_dbtemplate_i_przebuduj — guard + rebuild deno…
mpasternak Jul 8, 2026
3ddaecb
feat(opis): SzablonDlaOpisu FK->nazwa_szablonu + migracja kasująca wi…
mpasternak Jul 8, 2026
192294b
refactor(drop_dbtemplate): odsprzęgnięcie od FK + współdzielona funkc…
mpasternak Jul 8, 2026
51ce4ef
docs(plan): odzwierciedl poprawki Task 1/3 (tag-libs, FK-cleanup w te…
mpasternak Jul 8, 2026
806a046
fix(admin): SzablonDlaOpisu list_display + template_updated na nazwę …
mpasternak Jul 8, 2026
0120a7d
chore(tests): usuń nieużywaną fixture szablony (#329)
mpasternak Jul 8, 2026
b2588e2
docs(newsfragment): opis nadrzędnego z PBN + opis z dysku (FD#329)
mpasternak Jul 8, 2026
9100a79
fix(#329): DJ012 order + próg no_queries 11->12 (Site.get_current)
mpasternak Jul 9, 2026
3aac1ca
test(#329): utwardź kruchy test crossref na izolację shardów
mpasternak Jul 9, 2026
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

Large diffs are not rendered by default.

Large diffs are not rendered by default.

7 changes: 6 additions & 1 deletion src/api_v1/tests/test_wydawnictwo_ciagle.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,12 @@ def wiele_wydawnictw_ciaglych(db):
def test_rest_api_wydawnictwo_ciagle_no_queries(
wiele_wydawnictw_ciaglych, django_assert_max_num_queries, api_client
):
with django_assert_max_num_queries(11):
# 12 (było 11): wyrwanie opis_bibliograficzny.html z dbtemplates (#329)
# dokłada jedno Site.objects.get_current() (ścieżka loadera dbtemplates po
# skasowaniu wiersza). Stałe O(1), indeksowane po PK; w produkcji
# amortyzowane przez SITE_CACHE (pytest-django czyści go per-test — stąd
# widać to zapytanie tutaj, a nie na produkcji).
with django_assert_max_num_queries(12):
api_client.get(reverse("api_v1:wydawnictwo_ciagle-list"))


Expand Down
7 changes: 6 additions & 1 deletion src/api_v1/tests/test_wydawnictwo_zwarte.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,12 @@ def wiele_wydawnictw_zwartych(db):
def test_rest_api_wydawnictwo_zwarte_no_queries(
wiele_wydawnictw_zwartych, django_assert_max_num_queries, api_client
):
with django_assert_max_num_queries(11):
# 12 (było 11): wyrwanie opis_bibliograficzny.html z dbtemplates (#329)
# dokłada jedno Site.objects.get_current() (ścieżka loadera dbtemplates po
# skasowaniu wiersza). Stałe O(1), indeksowane po PK; w produkcji
# amortyzowane przez SITE_CACHE (pytest-django czyści go per-test — stąd
# widać to zapytanie tutaj, a nie na produkcji).
with django_assert_max_num_queries(12):
api_client.get(reverse("api_v1:wydawnictwo_zwarte-list"))


Expand Down
8 changes: 3 additions & 5 deletions src/bpp/admin/szablondlaopisubibliograficznego.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,15 @@

@admin.register(SzablonDlaOpisuBibliograficznego)
class SzablonDlaOpisuBibliograficznegoAdmin(admin.ModelAdmin):
list_display = ["model", "template"]
list_display = ["model", "nazwa_szablonu"]
empty_value_display = "(każdy)"

def save_model(self, request, obj: SzablonDlaOpisuBibliograficznego, form, change):
super(SzablonDlaOpisuBibliograficznegoAdmin, self).save_model(
request, obj, form, change
)
super().save_model(request, obj, form, change)
rebuild_instances_of_models(obj.get_models_for_this_szablon())

def delete_model(self, request, obj: SzablonDlaOpisuBibliograficznego):
super(SzablonDlaOpisuBibliograficznegoAdmin, self).delete_model(request, obj)
super().delete_model(request, obj)
rebuild_instances_of_models(
SzablonDlaOpisuBibliograficznego.objects.all_templated_models
)
4 changes: 3 additions & 1 deletion src/bpp/admin/templates.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ def template_updated(self, request, obj):
SzablonDlaOpisuBibliograficznego,
)

modele = SzablonDlaOpisuBibliograficznego.objects.get_models_for_template(obj)
modele = SzablonDlaOpisuBibliograficznego.objects.get_models_for_szablon(
obj.name
)

if not modele:
messages.info(
Expand Down
47 changes: 47 additions & 0 deletions src/bpp/dbtemplates_sync.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,50 @@ def wyczysc_cache_dbtemplate(name):
for loader in Engine.get_default().template_loaders:
if isinstance(loader, CachedLoader):
loader.get_template_cache.pop(loader.cache_key(name), None)


def usun_dbtemplate_i_przebuduj(name, modele, *, flush=False, log=None):
"""Skasuj wiersz dbtemplate ``name`` (render spadnie na dysk) i oznacz
``modele`` do przebudowy denorma. Współdzielone przez migrację i komendę
``drop_dbtemplate``.

GUARD: kasuje TYLKO gdy nazwa ma odpowiednik na dysku
(``disk_template_source(name) is not None``). Wiersz DB-only bez pliku na
dysku zostaje nietknięty — inaczej ``nazwa_szablonu`` zostałaby dyndająca i
``get_template`` rzucałby ``TemplateDoesNotExist`` przy każdym renderze /
flushu denorma. Zwraca ``False`` gdy pominięto (guard), ``True`` gdy
skasowano lub wiersza nie było mimo pliku na dysku.

``flush=True`` → synchroniczny ``denorms.flush()`` (komenda deployowa chce
odświeżyć od ręki); ``flush=False`` → tylko oznaczenie dirty (async kolejka
``denorm`` dokończy — użycie w migracji)."""
from dbtemplates.models import Template

from bpp.util import rebuild_instances_of_models
from bpp.util.dbtemplates_disk import disk_template_source

log = log or (lambda msg: None)

if disk_template_source(name) is None:
log(
f"[guard] '{name}' nie ma pliku na dysku — NIE kasuję wiersza "
f"dbtemplate (zostawiam, by nie zdyndać nazwy_szablonu)."
)
return False

tpl = Template.objects.filter(name=name).first()
if tpl is not None:
log(f"[usuwam dbtemplate '{name}'] backup treści:\n{tpl.content}")
tpl.delete()
# Cache dbtemplates nie znika sam (delete modelem historycznym nie
# odpala sygnałów); czyścimy synchronicznie jak drop_dbtemplate.
wyczysc_cache_dbtemplate(name)

if modele:
rebuild_instances_of_models(list(modele))
if flush:
from denorm import denorms

denorms.flush()

return True
67 changes: 6 additions & 61 deletions src/bpp/management/commands/compare_dbtemplates.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,15 @@
"""

import difflib
import logging
import sys
from pathlib import Path

from django.core.management.base import BaseCommand, CommandError
from django.template import TemplateDoesNotExist
from django.template.loader import get_template

from bpp.util import zaloguj_polkniety_wyjatek

try:
from dbtemplates.models import Template
except ImportError:
Template = None

logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = "Compare dbtemplates.models.Template instances with filesystem templates"
Expand Down Expand Up @@ -320,58 +312,11 @@ def _colorize_diff(self, diff_lines):
return colored_lines

def get_filesystem_template_content(self, template_name):
"""Get template content from filesystem"""
try:
# Get the template object to access its origin
django_template = get_template(template_name)

# Try to get the template source
if hasattr(django_template, "template") and hasattr(
django_template.template, "source"
):
return django_template.template.source

# Alternative approach: try to find and read the template file directly
from django.template.loader import find_template

try:
template_obj, origin = find_template(template_name)
if hasattr(origin, "name") and origin.name:
# Try to read the file directly
template_path = Path(origin.name)
if template_path.exists():
return template_path.read_text(encoding="utf-8")
except (TemplateDoesNotExist, AttributeError):
pass

# Fallback: render template to get content (might not be exact source)
# This won't work for templates with context variables, but it's a fallback
try:
from django.template import Context

rendered = django_template.render(Context({}))
return rendered
except Exception:
zaloguj_polkniety_wyjatek(
f"Renderowanie szablonu jako fallback do odczytu źródła "
f"(template_name={template_name})",
logger=logger,
do_rollbar=False,
)
"""Źródło szablonu z DYSKU (z pominięciem loadera dbtemplates).

except TemplateDoesNotExist:
pass
except Exception as e:
zaloguj_polkniety_wyjatek(
f"Wczytywanie źródła szablonu do porównania "
f"(template_name={template_name})",
logger=logger,
do_rollbar=False,
)
# Log the error but continue
if hasattr(self, "stderr"):
self.stderr.write(
f"Warning: Could not load template {template_name}: {e}"
)
Dawniej używała ``get_template()``, który idzie łańcuchem loaderów z
dbtemplates na pierwszym miejscu — więc dla nazwy istniejącej w bazie
zwracała treść z DB i porównanie było DB-vs-DB (zawsze 'match')."""
from bpp.util.dbtemplates_disk import disk_template_source

return None
return disk_template_source(template_name)
89 changes: 21 additions & 68 deletions src/bpp/management/commands/drop_dbtemplate.py
Original file line number Diff line number Diff line change
@@ -1,27 +1,18 @@
"""Usuń wiersz(e) dbtemplate z bazy, żeby renderowanie spadło na zawsze
aktualny plik z dysku — i przebuduj zależny ``opis_bibliograficzny_cache``.
"""Usuń wiersz(e) dbtemplate z bazy (render spada na plik z dysku) i przebuduj
zależny ``opis_bibliograficzny_cache``.

Dlaczego to nie jest zwykłe ``Template.objects.filter(...).delete()``:
Po wyrwaniu opisu z dbtemplates (#329) ``SzablonDlaOpisuBibliograficznego`` nie
ma już FK do ``Template`` — trzyma tylko ``nazwa_szablonu``. Komenda przestała
więc kasować powiązania; kasuje sam wiersz dbtemplate (z guardem dysk-existence)
i przebudowuje denorm dla modeli mapowanych na tę nazwę."""

1. ``SzablonDlaOpisuBibliograficznego.template`` jest ``on_delete=PROTECT``,
a migracja zasiewa domyślne powiązanie ``model=None`` -> ``opis_...``.
Bez wcześniejszego usunięcia tego powiązania ``Template.delete()`` rzuca
``ProtectedError``.
2. ``opis_bibliograficzny_cache`` to pole ``@denormalized``, które NIE zależy
od dbtemplate. Samo usunięcie wiersza nie odświeży zapisanego stringa —
trzeba wymusić ``rebuild_instances_of_models`` (a trigger ``bpp_refresh_cache``
dociągnie kopię w ``Rekord``).
"""

from dbtemplates.models import Template
from django.core.management.base import BaseCommand
from django.db import transaction

from bpp.dbtemplates_sync import wyczysc_cache_dbtemplate
from bpp.dbtemplates_sync import usun_dbtemplate_i_przebuduj
from bpp.models.szablondlaopisubibliograficznego import (
SzablonDlaOpisuBibliograficznego,
)
from bpp.util import rebuild_instances_of_models


class Command(BaseCommand):
Expand All @@ -44,60 +35,22 @@ def add_arguments(self, parser):

@transaction.atomic
def handle(self, *args, **options):
modele_do_przebudowy = set()

for name in options["template_names"]:
template = Template.objects.filter(name=name).first()
if template is None:
modele = (
[]
if options["skip_rebuild"]
else SzablonDlaOpisuBibliograficznego.objects.get_models_for_szablon(
name
)
)
usunieto = usun_dbtemplate_i_przebuduj(
name, modele, flush=True, log=self.stdout.write
)
if usunieto:
self.stdout.write(self.style.SUCCESS(f"Przetworzono '{name}'."))
else:
self.stderr.write(
self.style.WARNING(
f"Szablon '{name}' nie istnieje w bazie — pomijam."
f"'{name}' nie ma pliku na dysku — pominięto (guard)."
)
)
continue

# Modele zależne MUSZĄ być policzone PRZED usunięciem powiązań,
# bo get_models_for_template czyta tabelę SzablonDlaOpisu...
modele = SzablonDlaOpisuBibliograficznego.objects.get_models_for_template(
template
)
modele_do_przebudowy.update(modele)

# 1. Zdejmij PROTECT-ujące powiązania.
usuniete, _ = SzablonDlaOpisuBibliograficznego.objects.filter(
template=template
).delete()
# 2. Usuń sam wiersz dbtemplate -> get_template spada na dysk.
template.delete()
# 3. Wyczyść cache dbtemplates/CachedLoader, inaczej loader nadal
# serwowałby usuniętą treść (i przebudowa cache renderowałaby
# stary szablon zamiast dyskowego).
wyczysc_cache_dbtemplate(name)

self.stdout.write(
self.style.SUCCESS(
f"Usunięto '{name}' (oraz {usuniete} powiązań Szablonu)."
)
)

# 3. Przebuduj cache z dysku (chyba że poproszono o pominięcie).
if options["skip_rebuild"]:
return
if not modele_do_przebudowy:
self.stdout.write(
"Brak modeli zależnych od usuniętych szablonów — bez przebudowy."
)
return
self.stdout.write(
f"Przebudowa opis_bibliograficzny_cache dla {len(modele_do_przebudowy)} "
"modeli (z dysku)…"
)
# rebuild_instances_of_models tylko ZNACZY instancje jako dirty
# (DirtyInstance); realne przeliczenie robi denorm.flush(). Admin
# świadomie odkłada flush na noc, ale komenda deployowa ma odświeżyć
# cache od ręki.
rebuild_instances_of_models(list(modele_do_przebudowy))
from denorm import denorms

denorms.flush()
self.stdout.write(self.style.SUCCESS("Gotowe."))
65 changes: 65 additions & 0 deletions src/bpp/migrations/0468_szablon_nazwa_szablonu.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
from django.db import migrations, models


def backfill_nazwa(apps, schema_editor):
Szablon = apps.get_model("bpp", "SzablonDlaOpisuBibliograficznego")
Template = apps.get_model("dbtemplates", "Template")
for row in Szablon.objects.all():
if row.template_id:
row.nazwa_szablonu = Template.objects.get(pk=row.template_id).name
row.save(update_fields=["nazwa_szablonu"])


def purge_opis_dbtemplate(apps, schema_editor):
# Konkretne klasy modeli (denorm rebuild jak w drop_dbtemplate). Import w
# ciele funkcji — bezpieczny w tym punkcie migracji.
from bpp.dbtemplates_sync import usun_dbtemplate_i_przebuduj
from bpp.models.patent import Patent
from bpp.models.praca_doktorska import Praca_Doktorska
from bpp.models.praca_habilitacyjna import Praca_Habilitacyjna
from bpp.models.wydawnictwo_ciagle import Wydawnictwo_Ciagle
from bpp.models.wydawnictwo_zwarte import Wydawnictwo_Zwarte

modele = [
Wydawnictwo_Ciagle,
Wydawnictwo_Zwarte,
Praca_Doktorska,
Praca_Habilitacyjna,
Patent,
]
Szablon = apps.get_model("bpp", "SzablonDlaOpisuBibliograficznego")
nazwy = {n for n in Szablon.objects.values_list("nazwa_szablonu", flat=True) if n}
for name in sorted(nazwy):
# guard (dysk) + log + delete + czyszczenie cache + oznaczenie dirty.
# flush=False -> async kolejka denorm dokończy (migracja nieblokująca).
usun_dbtemplate_i_przebuduj(name, modele, flush=False, log=print)


class Migration(migrations.Migration):
atomic = False

dependencies = [
("bpp", "0467_seed_crossref_mapper_rows"),
("dbtemplates", "0002_alter_template_creation_date_and_more"),
]

operations = [
migrations.AddField(
model_name="szablondlaopisubibliograficznego",
name="nazwa_szablonu",
field=models.CharField(
default="opis_bibliograficzny.html",
help_text=(
"Nazwa szablonu Django ładowanego z dysku, "
"np. opis_bibliograficzny.html"
),
max_length=255,
),
),
migrations.RunPython(backfill_nazwa, migrations.RunPython.noop),
migrations.RemoveField(
model_name="szablondlaopisubibliograficznego",
name="template",
),
migrations.RunPython(purge_opis_dbtemplate, migrations.RunPython.noop),
]
Loading
Loading