- plik {{ object.plik.name }} {% if object.uczelnia %} {{ object.uczelnia }} {% endif %}:
+ plik {{ object.plik.name }} {% if object.uczelnia %} {{ object.uczelnia }} {% endif %}:
import utworzono {{ object.created_on }}{% if object.finished_on %}, ukończono {{ object.finished_on }}
{% if object.finished_successfully %}, zakończono pomyślnie{% else %}, zakończono z błędem{% endif %}
{% endif %}
diff --git a/src/import_polon/templates/import_polon/potwierdz_zapis_do_bazy.html b/src/import_polon/templates/import_polon/potwierdz_zapis_do_bazy.html
index e3bab8b27..b60e24927 100644
--- a/src/import_polon/templates/import_polon/potwierdz_zapis_do_bazy.html
+++ b/src/import_polon/templates/import_polon/potwierdz_zapis_do_bazy.html
@@ -20,6 +20,6 @@ Zapisać import do bazy danych?
Tak, zapisz do bazy danych
- Anuluj
+ Anuluj
{% endblock %}
diff --git a/src/import_polon/templates/import_polon/wierszimportuplikuabsencji_list.html b/src/import_polon/templates/import_polon/wierszimportuplikuabsencji_list.html
index dc0579b07..57828933d 100644
--- a/src/import_polon/templates/import_polon/wierszimportuplikuabsencji_list.html
+++ b/src/import_polon/templates/import_polon/wierszimportuplikuabsencji_list.html
@@ -33,8 +33,6 @@ Zmiany nie zostały wprowadzone do bazy danych. Import został uruchomi
{% endif %}
- {% include "long_running/operation_details.html" %}
-
{% if object.finished_successfully %}
{% include "pagination.html" %}
diff --git a/src/import_polon/templates/import_polon/wierszimportuplikupolon_list.html b/src/import_polon/templates/import_polon/wierszimportuplikupolon_list.html
index d7644f079..2bcb79247 100644
--- a/src/import_polon/templates/import_polon/wierszimportuplikupolon_list.html
+++ b/src/import_polon/templates/import_polon/wierszimportuplikupolon_list.html
@@ -182,32 +182,6 @@ Zmiany nie zostały wprowadzone do bazy danych. Import został uruchomi
{% endif %}
-
- Pokaż szczegóły operacji
-
-
- {% include "long_running/operation_details.html" %}
-
-
-
{% if object.finished_successfully and unmatched_count > 0 %}
Autorzy z przypisaniami dyscyplin w BPP, którzy nie byli w pliku Excel ({{ unmatched_count }})
diff --git a/src/import_polon/tests/test_anon_probe.py b/src/import_polon/tests/test_anon_probe.py
new file mode 100644
index 000000000..c2292a26e
--- /dev/null
+++ b/src/import_polon/tests/test_anon_probe.py
@@ -0,0 +1,21 @@
+import uuid
+
+import pytest
+from django.urls import reverse
+
+
+@pytest.mark.django_db
+def test_anon_utworz_nie_daje_500(client):
+ """Anon GET na gejtowany liveops CreateView — 302→login, NIE 500."""
+ resp = client.get(reverse("import_polon:utworz-import"))
+ assert resp.status_code != 500, "500-for-anon (handle_no_permission MRO bug)"
+ assert resp.status_code == 302
+
+
+@pytest.mark.django_db
+def test_anon_restart_nie_daje_500(client):
+ """Anon POST na gejtowany liveops RestartView — 302→login, NIE 500."""
+ url = reverse("import_polon:importplikupolon-restart", args=[uuid.uuid4()])
+ resp = client.post(url)
+ assert resp.status_code != 500, "500-for-anon (handle_no_permission MRO bug)"
+ assert resp.status_code == 302
diff --git a/src/import_polon/tests/test_import_absencji_core.py b/src/import_polon/tests/test_import_absencji_core.py
index f4ceb55a9..bc3aa73b4 100644
--- a/src/import_polon/tests/test_import_absencji_core.py
+++ b/src/import_polon/tests/test_import_absencji_core.py
@@ -1,4 +1,5 @@
import pytest
+from liveops.testing import MockProgress
from model_bakery import baker
from bpp.models import Autor
@@ -19,7 +20,7 @@ def test_analyze_excel_file_import_absencji(fn_test_import_absencji):
)
adam_nowak_2: Autor = baker.make(Autor, nazwisko="Nowak", imiona="Adam")
- analyze_file_import_absencji(fn_test_import_absencji, ipa)
+ analyze_file_import_absencji(fn_test_import_absencji, ipa, MockProgress(ipa))
assert jan_kowalski.autor_absencja_set.get(rok=2017).ile_dni == 141
assert jan_kowalski.autor_absencja_set.get(rok=2018).ile_dni == 352
@@ -32,7 +33,7 @@ def test_analyze_excel_file_import_absencji(fn_test_import_absencji):
def test_analyze_excel_file_import_absencji_zly_plik(fn_test_import_polon):
ipa = baker.make(ImportPlikuAbsencji, zapisz_zmiany_do_bazy=True)
- analyze_file_import_absencji(fn_test_import_polon, ipa)
+ analyze_file_import_absencji(fn_test_import_polon, ipa, MockProgress(ipa))
@pytest.mark.django_db
diff --git a/src/import_polon/tests/test_import_polon_core.py b/src/import_polon/tests/test_import_polon_core.py
index df22e4a04..7bfb757f3 100644
--- a/src/import_polon/tests/test_import_polon_core.py
+++ b/src/import_polon/tests/test_import_polon_core.py
@@ -11,6 +11,7 @@
from denorm import denorms
from django.core.files import File
from django.db import transaction
+from liveops.testing import MockProgress
from model_bakery import baker
from bpp.models import (
@@ -30,7 +31,7 @@ def test_analyze_excel_file_import_polon_zly_plik(fn_test_import_absencji):
ipp: ImportPlikuPolon = baker.make(
ImportPlikuPolon, zapisz_zmiany_do_bazy=True, rok=ROK
)
- analyze_file_import_polon(fn_test_import_absencji, ipp)
+ analyze_file_import_polon(fn_test_import_absencji, ipp, MockProgress(ipp))
@pytest.mark.django_db
@@ -42,7 +43,7 @@ def test_analyze_excel_file_import_polon_plik_bez_dyscyplin(
ImportPlikuPolon, zapisz_zmiany_do_bazy=True, rok=ROK
)
baker.make(Autor, nazwisko="Kowalski", imiona="Aleksander Bolesław")
- analyze_file_import_polon(fn_test_import_polon_bledny, ipp)
+ analyze_file_import_polon(fn_test_import_polon_bledny, ipp, MockProgress(ipp))
def test_analyze_excel_file_import_polon(
@@ -109,7 +110,7 @@ def test_analyze_excel_file_import_polon(
rodzaj_autora=rodzaj_autora_z,
)
- analyze_file_import_polon(fn_test_import_polon, ipp)
+ analyze_file_import_polon(fn_test_import_polon, ipp, MockProgress(ipp))
assert (
artur_dyscyplinazn.autor_dyscyplina_set.get(rok=ROK).rodzaj_autora
@@ -157,7 +158,7 @@ def test_analyze_file_import_polon_with_invalid_zatrudnienie(tmp_path, uczelnia)
import_model.plik.save("test_invalid_zatrudnienie.xlsx", File(f))
# Run import
- analyze_file_import_polon(str(test_file), import_model)
+ analyze_file_import_polon(str(test_file), import_model, MockProgress(import_model))
# Check results - both records should be ignored
results = WierszImportuPlikuPolon.objects.filter(parent=import_model)
@@ -203,7 +204,7 @@ def test_analyze_file_import_polon_with_valid_zatrudnienie(tmp_path, uczelnia):
import_model.plik.save("test_valid_zatrudnienie.xlsx", File(f))
# Run import
- analyze_file_import_polon(str(test_file), import_model)
+ analyze_file_import_polon(str(test_file), import_model, MockProgress(import_model))
# Check results - record should be processed (not ignored)
results = WierszImportuPlikuPolon.objects.filter(parent=import_model)
@@ -238,7 +239,7 @@ def test_analyze_excel_file_import_polon_badawczy_type(
)
# Run import
- analyze_file_import_polon(fn_test_import_polon, ipp)
+ analyze_file_import_polon(fn_test_import_polon, ipp, MockProgress(ipp))
# Check if author was classified as type 'B' based on GRUPA_STANOWISK
try:
diff --git a/src/import_polon/tests/test_import_polon_ignoruj.py b/src/import_polon/tests/test_import_polon_ignoruj.py
index 0905cda6a..5ee90c0a4 100644
--- a/src/import_polon/tests/test_import_polon_ignoruj.py
+++ b/src/import_polon/tests/test_import_polon_ignoruj.py
@@ -8,6 +8,7 @@
import pandas as pd
import pytest
from django.core.files import File
+from liveops.testing import MockProgress
from model_bakery import baker
from bpp.models import Autor
@@ -46,7 +47,7 @@ def test_analyze_file_import_polon_ignoruj_miejsce_pracy_true(tmp_path, uczelnia
import_model.plik.save("test_ignoruj_miejsce_pracy.xlsx", File(f))
# Run import
- analyze_file_import_polon(str(test_file), import_model)
+ analyze_file_import_polon(str(test_file), import_model, MockProgress(import_model))
# Check results - records should NOT be ignored due to ZATRUDNIENIE
results = WierszImportuPlikuPolon.objects.filter(parent=import_model)
@@ -88,7 +89,7 @@ def test_analyze_file_import_polon_ignoruj_miejsce_pracy_false_default(
import_model.plik.save("test_ignoruj_miejsce_pracy_false.xlsx", File(f))
# Run import
- analyze_file_import_polon(str(test_file), import_model)
+ analyze_file_import_polon(str(test_file), import_model, MockProgress(import_model))
# Check results - record should be ignored
results = WierszImportuPlikuPolon.objects.filter(parent=import_model)
@@ -142,7 +143,7 @@ def test_analyze_file_import_polon_ignoruj_miejsce_pracy_processes_author(
import_model.plik.save("test_ignoruj_autor.xlsx", File(f))
# Run import
- analyze_file_import_polon(str(test_file), import_model)
+ analyze_file_import_polon(str(test_file), import_model, MockProgress(import_model))
# Check that author was matched and processed
results = WierszImportuPlikuPolon.objects.filter(parent=import_model)
diff --git a/src/import_polon/tests/test_import_polon_override.py b/src/import_polon/tests/test_import_polon_override.py
index a48eaeb93..a16384123 100644
--- a/src/import_polon/tests/test_import_polon_override.py
+++ b/src/import_polon/tests/test_import_polon_override.py
@@ -8,6 +8,7 @@
import pandas as pd
import pytest
from django.core.files import File
+from liveops.testing import MockProgress
from model_bakery import baker
from bpp.models import Autor
@@ -59,7 +60,7 @@ def test_import_polon_override_badawczy_true(
import_model.plik.save("test_override_true.xlsx", File(f))
# Run import
- analyze_file_import_polon(str(test_file), import_model)
+ analyze_file_import_polon(str(test_file), import_model, MockProgress(import_model))
# Check if author was classified as type 'B' due to override
ad = autor.autor_dyscyplina_set.get(rok=2023)
@@ -110,7 +111,7 @@ def test_import_polon_override_badawczy_false(
import_model.plik.save("test_override_false.xlsx", File(f))
# Run import
- analyze_file_import_polon(str(test_file), import_model)
+ analyze_file_import_polon(str(test_file), import_model, MockProgress(import_model))
# Check if author was classified as type 'Z' (not B) due to wymuszenie
ad = autor.autor_dyscyplina_set.get(rok=2023)
@@ -157,7 +158,7 @@ def test_import_polon_no_override_uses_default_logic(
import_model.plik.save("test_no_override.xlsx", File(f))
# Run import
- analyze_file_import_polon(str(test_file), import_model)
+ analyze_file_import_polon(str(test_file), import_model, MockProgress(import_model))
# Check if author was classified as type 'B' using default logic
ad = autor.autor_dyscyplina_set.get(rok=2023)
@@ -207,7 +208,7 @@ def test_import_polon_override_case_insensitive(
import_model.plik.save("test_override_case.xlsx", File(f))
# Run import
- analyze_file_import_polon(str(test_file), import_model)
+ analyze_file_import_polon(str(test_file), import_model, MockProgress(import_model))
# Check if author was classified as type 'B' despite different case
ad = autor.autor_dyscyplina_set.get(rok=2023)
diff --git a/src/import_polon/tests/test_liveops.py b/src/import_polon/tests/test_liveops.py
new file mode 100644
index 000000000..c5e548296
--- /dev/null
+++ b/src/import_polon/tests/test_liveops.py
@@ -0,0 +1,140 @@
+"""Testy migracji importów POLON/absencji na django-liveops.
+
+Pokrywają: finalizację ``run(self, p)`` (MockProgress), renderowanie strony
+live (host template + wrapper CSRF) oraz gejtowany grupą restart.
+"""
+
+import pytest
+from django.contrib.auth.models import Group
+from django.core.files import File
+from django.urls import reverse
+from liveops.testing import MockProgress
+from model_bakery import baker
+
+from import_polon.models import (
+ ImportPlikuAbsencji,
+ ImportPlikuPolon,
+ WierszImportuPlikuPolon,
+)
+
+GRUPA = "wprowadzanie danych"
+
+
+# --- run() finalizuje operację ----------------------------------------------
+
+
+@pytest.mark.django_db
+def test_run_finalizuje_polon(admin_user, fn_test_import_polon):
+ imp = baker.make(
+ ImportPlikuPolon, owner=admin_user, rok=2023, zapisz_zmiany_do_bazy=False
+ )
+ with open(fn_test_import_polon, "rb") as f:
+ imp.plik.save("test_import_polon.xlsx", File(f))
+
+ imp.run(MockProgress(imp))
+
+ imp.refresh_from_db()
+ assert imp.finished_successfully is True
+ n = imp.get_details_set().count()
+ assert n > 0, "run() musi utworzyć wiersze-dzieci"
+ assert imp.result_context == {"total": n}
+
+
+@pytest.mark.django_db
+def test_run_finalizuje_absencji(admin_user, fn_test_import_absencji):
+ imp = baker.make(ImportPlikuAbsencji, owner=admin_user, zapisz_zmiany_do_bazy=False)
+ with open(fn_test_import_absencji, "rb") as f:
+ imp.plik.save("test_import_absencji.xlsx", File(f))
+
+ imp.run(MockProgress(imp))
+
+ imp.refresh_from_db()
+ assert imp.finished_successfully is True
+ n = imp.get_details_set().count()
+ assert n > 0
+ assert imp.result_context == {"total": n}
+
+
+# --- strona live (host template + wrapper CSRF) -----------------------------
+
+
+@pytest.mark.django_db
+def test_liveops_live_view_renderuje_host(admin_client, admin_user):
+ imp = baker.make(ImportPlikuPolon, owner=admin_user, rok=2023)
+
+ assert imp.get_absolute_url() == (f"/live/import_polon.importplikupolon/{imp.pk}/")
+
+ response = admin_client.get(imp.get_absolute_url())
+
+ assert response.status_code == 200
+ content = response.content.decode()
+ # kontener liveops (WS binding)
+ assert "data-liveop-channel" in content
+ assert "data-liveop-token" in content
+ # wrapper CSRF (CSRF_COOKIE_HTTPONLY=True → X-CSRFToken przez hx-headers)
+ assert "X-CSRFToken" in content
+
+
+@pytest.mark.django_db
+def test_liveops_live_view_absencji_url(admin_user):
+ imp = baker.make(ImportPlikuAbsencji, owner=admin_user)
+ assert imp.get_absolute_url() == (
+ f"/live/import_polon.importplikuabsencji/{imp.pk}/"
+ )
+
+
+# --- restart gejtowany grupą (precedens #508 F4) ----------------------------
+
+
+def _polon_z_wierszami(owner):
+ """Zakończony pomyślnie import POLON z dwoma wierszami-dziećmi (bez pliku —
+ ponowny run i tak by padł, ale nas interesuje wyłącznie efekt uboczny
+ bramki: czy wiersze-dzieci zostały skasowane / operacja zresetowana)."""
+ from django.utils import timezone
+
+ imp = baker.make(
+ ImportPlikuPolon,
+ owner=owner,
+ rok=2023,
+ zapisz_zmiany_do_bazy=False,
+ started_on=timezone.now(),
+ finished_on=timezone.now(),
+ finished_successfully=True,
+ )
+ baker.make(WierszImportuPlikuPolon, parent=imp, nr_wiersza=1)
+ baker.make(WierszImportuPlikuPolon, parent=imp, nr_wiersza=2)
+ return imp
+
+
+@pytest.mark.django_db
+def test_restart_bez_grupy_nie_zmienia_stanu(client, django_user_model):
+ u = django_user_model.objects.create_user(username="plain-restart", password="x")
+ client.force_login(u)
+ imp = _polon_z_wierszami(u)
+ url = reverse("import_polon:importplikupolon-restart", kwargs={"pk": imp.pk})
+
+ resp = client.post(url)
+
+ # GroupRequiredMixin ma raise_exception=False → 302 (redirect_to_login),
+ # NIE 403. Kluczowe: brak efektu ubocznego.
+ assert resp.status_code == 302
+ imp.refresh_from_db()
+ assert imp.finished_successfully is True, "operacja NIE mogła zostać zresetowana"
+ assert imp.wierszimportuplikupolon_set.count() == 2, "wiersze-dzieci nietknięte"
+
+
+@pytest.mark.django_db
+def test_restart_z_grupa_przechodzi(client, django_user_model):
+ u = django_user_model.objects.create_user(username="entry-restart", password="x")
+ u.groups.add(Group.objects.get_or_create(name=GRUPA)[0])
+ client.force_login(u)
+ imp = _polon_z_wierszami(u)
+ url = reverse("import_polon:importplikupolon-restart", kwargs={"pk": imp.pk})
+
+ client.post(url)
+
+ imp.refresh_from_db()
+ # Bramka przeszła: on_restart() skasował wiersze-dzieci, a reset zdjął
+ # znacznik pomyślnego zakończenia (ponowny run bez pliku kończy się błędem).
+ assert imp.wierszimportuplikupolon_set.count() == 0
+ assert imp.finished_successfully is False
diff --git a/src/import_polon/tests/test_multi_uczelnia_scoping.py b/src/import_polon/tests/test_multi_uczelnia_scoping.py
index 26fca98a9..e8048a20c 100644
--- a/src/import_polon/tests/test_multi_uczelnia_scoping.py
+++ b/src/import_polon/tests/test_multi_uczelnia_scoping.py
@@ -17,6 +17,7 @@
import pandas as pd
import pytest
from django.utils import timezone
+from liveops.testing import MockProgress
from model_bakery import baker
from bpp.models import Autor, Jednostka, Uczelnia, Wydzial
@@ -189,7 +190,7 @@ def test_walidacja_zatrudnienia_zawezona_do_uczelni_importu(tmp_path):
zapisz_zmiany_do_bazy=False,
ukryj_niezmatchowanych_autorow=False,
)
- analyze_file_import_polon(str(test_file), import_model)
+ analyze_file_import_polon(str(test_file), import_model, MockProgress(import_model))
wiersze = {w.dane_z_xls["NAZWISKO"]: w for w in import_model.get_details_set()}
assert "REKORD ZIGNOROWANY" not in wiersze["Iksinski"].rezultat, (
@@ -236,7 +237,7 @@ def test_import_nie_modyfikuje_autora_innej_uczelni(tmp_path):
ignoruj_miejsce_pracy=True,
ukryj_niezmatchowanych_autorow=False,
)
- analyze_file_import_polon(str(test_file), import_model)
+ analyze_file_import_polon(str(test_file), import_model, MockProgress(import_model))
wiersze = {w.dane_z_xls["NAZWISKO"]: w for w in import_model.get_details_set()}
assert "innej uczelni" in wiersze["Igrekowa"].rezultat.lower(), (
diff --git a/src/import_polon/tests/test_zapisz_do_bazy.py b/src/import_polon/tests/test_zapisz_do_bazy.py
index cfee61eda..3806acf91 100644
--- a/src/import_polon/tests/test_zapisz_do_bazy.py
+++ b/src/import_polon/tests/test_zapisz_do_bazy.py
@@ -20,10 +20,10 @@ def _reimport_scheduled(callbacks):
"""Ile spośród callbacków ``on_commit`` to nasze ponowne uruchomienie importu.
Middleware audytu (django-easy-audit) rejestruje własne callbacki, więc nie
- można liczyć wszystkich — filtrujemy po tym, który pochodzi z
- ``LongRunningTaskCallerMixin.task_on_commit``.
+ można liczyć wszystkich — filtrujemy po ``LiveOperation.enqueue``, który
+ ``ZapiszDoBazyMixin.post`` odracza przez ``transaction.on_commit``.
"""
- return sum("task_on_commit" in cb.__qualname__ for cb in callbacks)
+ return sum("enqueue" in cb.__qualname__ for cb in callbacks)
def _finished_dry_run(model, owner, **extra):
diff --git a/src/import_polon/urls.py b/src/import_polon/urls.py
index 15644b556..5c0549a33 100644
--- a/src/import_polon/urls.py
+++ b/src/import_polon/urls.py
@@ -7,16 +7,6 @@
urlpatterns = [
path("dane/", views.PokazImporty.as_view(), name="index"),
path("dane/nowy/", views.UtworzImportPlikuPolon.as_view(), name="utworz-import"),
- path(
- "dane//",
- views.ImportPolonRouterView.as_view(),
- name="importplikupolon-router",
- ),
- path(
- "dane//details/",
- views.ImportPolonDetailsView.as_view(),
- name="importplikupolon-details",
- ),
path(
"dane//regen/",
views.RestartImportView.as_view(),
@@ -32,22 +22,12 @@
views.ImportPolonResultsView.as_view(),
name="importplikupolon-results",
),
- path("absencje/", views.PokazImporty.as_view(), name="index-absencji"),
+ path("absencje/", views.PokazImportyAbsencji.as_view(), name="index-absencji"),
path(
"absencje/nowy/",
views.UtworzImportPlikuAbsencji.as_view(),
name="utworz-import-absencji",
),
- path(
- "absencje//",
- views.ImportAbsencjiRouterView.as_view(),
- name="importplikuabsencji-router",
- ),
- path(
- "absencje//details/",
- views.ImportAbsencjiDetailsView.as_view(),
- name="importplikuabsencji-details",
- ),
path(
"absencje//regen/",
views.RestartImportAbsencjiView.as_view(),
diff --git a/src/import_polon/views/plik_absencji.py b/src/import_polon/views/plik_absencji.py
index 1aabe03cc..b0a6b0717 100644
--- a/src/import_polon/views/plik_absencji.py
+++ b/src/import_polon/views/plik_absencji.py
@@ -1,13 +1,14 @@
+from django.shortcuts import get_object_or_404
+from django.utils.functional import cached_property
+from django.views.generic import ListView
+from liveops.views import CreateLiveOperationView
+
from import_polon.forms import NowyImportAbsencjiForm
from import_polon.models import ImportPlikuAbsencji
-from import_polon.views.plik_polon import BaseImportPlikuPolonMixin, ZapiszDoBazyMixin
-from long_running.views import (
- CreateLongRunningOperationView,
- LongRunningDetailsView,
- LongRunningOperationsView,
- LongRunningResultsView,
- LongRunningRouterView,
- RestartLongRunningOperationView,
+from import_polon.views.plik_polon import (
+ BaseImportPlikuPolonMixin,
+ ZapiszDoBazyMixin,
+ _PkOwnerRestartMixin,
)
@@ -15,33 +16,51 @@ class BaseImportPlikuAbsencjiMixin(BaseImportPlikuPolonMixin):
model = ImportPlikuAbsencji
-class PokazImportPlikuAbsencji(BaseImportPlikuAbsencjiMixin, LongRunningOperationsView):
- pass
-
+class PokazImportyAbsencji(BaseImportPlikuAbsencjiMixin, ListView):
+ """Lista importów absencji bieżącego użytkownika (owner-scoped).
-class UtworzImportPlikuAbsencji(
- BaseImportPlikuAbsencjiMixin, CreateLongRunningOperationView
-):
- form_class = NowyImportAbsencjiForm
+ Absencje NIE mają pola ``uczelnia`` (import globalny, nie per-uczelnia),
+ więc bez zawężenia multi-hosted. Bugfix: dawniej ``index-absencji``
+ mapował na ``PokazImporty`` (listę POLON) — teraz ma własny widok +
+ szablon ``importplikuabsencji_list.html``.
+ """
+ max_previous_ops = 10
+ template_name = "import_polon/importplikuabsencji_list.html"
-class ImportAbsencjiRouterView(BaseImportPlikuAbsencjiMixin, LongRunningRouterView):
- redirect_prefix = "import_polon:importplikabsencji"
+ def get_queryset(self):
+ qset = self.model.objects.filter(owner=self.request.user).order_by(
+ "-created_on"
+ )
+ for elem in qset[self.max_previous_ops :]:
+ elem.delete()
+ return qset
-class ImportAbsencjiDetailsView(BaseImportPlikuAbsencjiMixin, LongRunningDetailsView):
- pass
+class UtworzImportPlikuAbsencji(BaseImportPlikuAbsencjiMixin, CreateLiveOperationView):
+ form_class = NowyImportAbsencjiForm
-class RestartImportAbsencjiView(
- BaseImportPlikuAbsencjiMixin, RestartLongRunningOperationView
-):
- pass
+class RestartImportAbsencjiView(_PkOwnerRestartMixin):
+ model = ImportPlikuAbsencji
class ZapiszDoBazyImportAbsencjiView(BaseImportPlikuAbsencjiMixin, ZapiszDoBazyMixin):
pass
-class ImportAbsencjiResultsView(BaseImportPlikuAbsencjiMixin, LongRunningResultsView):
- pass
+class ImportAbsencjiResultsView(BaseImportPlikuAbsencjiMixin, ListView):
+ paginate_by = 25
+
+ @cached_property
+ def parent_object(self):
+ return get_object_or_404(
+ ImportPlikuAbsencji, pk=self.kwargs["pk"], owner=self.request.user
+ )
+
+ def get_queryset(self):
+ return self.parent_object.get_details_set()
+
+ def get_context_data(self, **kwargs):
+ # object=self.parent_object OBOWIĄZKOWE (szablon woła {% url ... object.pk %}).
+ return super().get_context_data(object=self.parent_object, **kwargs)
diff --git a/src/import_polon/views/plik_polon.py b/src/import_polon/views/plik_polon.py
index 8e64a899b..ed1129e05 100644
--- a/src/import_polon/views/plik_polon.py
+++ b/src/import_polon/views/plik_polon.py
@@ -1,36 +1,37 @@
from braces.views import GroupRequiredMixin
+from django.contrib.auth.mixins import LoginRequiredMixin
from django.db import transaction
from django.db.models import Q
from django.http import HttpResponseRedirect
-from django.views.generic import DetailView
+from django.shortcuts import get_object_or_404
+from django.utils.functional import cached_property
+from django.views.generic import DetailView, ListView
+from liveops.views import CreateLiveOperationView, RestartView
from import_polon.forms import NowyImportForm, WierszImportuPlikuPolonFilterForm
from import_polon.models import ImportPlikuPolon
-from long_running.views import (
- CreateLongRunningOperationView,
- LongRunningDetailsView,
- LongRunningOperationsView,
- LongRunningResultsView,
- LongRunningRouterView,
- LongRunningTaskCallerMixin,
- RestartLongRunningOperationView,
- RestrictToOwnerMixin,
-)
-
-
-class ZapiszDoBazyMixin(RestrictToOwnerMixin, LongRunningTaskCallerMixin, DetailView):
+
+
+class ZapiszDoBazyMixin(LoginRequiredMixin, DetailView):
"""Domknięcie dry-runa zapisem do bazy.
Gdy import uruchomiono bez zapisu (``zapisz_zmiany_do_bazy=False``), ta akcja
przestawia flagę na ``True`` i resetuje obiekt do ponownego uruchomienia —
„taki reset, ALE tym razem modyfikuj bazę". GET pokazuje stronę
potwierdzenia; dopiero POST modyfikuje bazę (prefetch/robot nie wywoła
- zapisu). ``mark_reset`` czyści stan i kasuje wiersze-dzieci; ponowne
- ``perform()`` — tym razem z zapisem — kolejkuje ``task_on_commit``.
+ zapisu). ``on_restart()`` kasuje wiersze-dzieci, ``reset_liveops_state()``
+ czyści stan operacji; ponowny ``enqueue()`` — tym razem z zapisem.
+
+ Owner-scope własnym ``get_queryset`` (bez ``long_running`` — Faza 4 wymaga
+ braku importów z tamtego pakietu). Bramka grupy dochodzi w podklasach
+ (``GroupRequiredMixin`` przez ``BaseImportPlikuPolonMixin``).
"""
template_name = "import_polon/potwierdz_zapis_do_bazy.html"
+ def get_queryset(self):
+ return self.model.objects.filter(owner=self.request.user)
+
@transaction.atomic
def post(self, *args, **kwargs):
# select_for_update: blokuje wiersz na czas transakcji, więc dwa
@@ -41,11 +42,18 @@ def post(self, *args, **kwargs):
# użytkownik musiał zobaczyć podgląd. Odsiewa import w trakcie, zakończony
# błędem oraz już zapisany do bazy (ochrona przed podwójnym zapisem).
if self.object.finished_successfully and not self.object.zapisz_zmiany_do_bazy:
- # Flaga trafi do bazy przez pełny save() w mark_reset() poniżej.
self.object.zapisz_zmiany_do_bazy = True
- self.object.mark_reset()
- self.task_on_commit(pk=self.object.pk)
- return HttpResponseRedirect("..")
+ self.object.on_restart() # kasuje wiersze-dzieci poprzedniego runu
+ pola = self.object.reset_liveops_state()
+ self.object.save(update_fields=["zapisz_zmiany_do_bazy", *pola])
+ # ``enqueue()`` przy RUNNER="celery" woła .delay() BEZ on_commit —
+ # w bloku atomic worker mógłby czytać stan sprzed commitu. Odraczamy
+ # do on_commit, żeby ponowny przebieg widział flagę + wyczyszczony
+ # stan już utrwalone w bazie.
+ transaction.on_commit(self.object.enqueue)
+ # Stary „.." (router URL) kasowany w tej fazie → 404. Kierujemy na
+ # centralną stronę live liveops.
+ return HttpResponseRedirect(self.object.get_absolute_url())
class BaseImportPlikuPolonMixin(GroupRequiredMixin):
@@ -53,7 +61,16 @@ class BaseImportPlikuPolonMixin(GroupRequiredMixin):
model = ImportPlikuPolon
-class PokazImporty(BaseImportPlikuPolonMixin, LongRunningOperationsView):
+class PokazImporty(BaseImportPlikuPolonMixin, ListView):
+ """Lista importów bieżącego użytkownika (owner-scoped, zawężona do uczelni).
+
+ Dawniej ``long_running.LongRunningOperationsView``; teraz zwykły ListView.
+ Strona live (postęp/wynik) jest osobno, pod centralnym ``liveops:live``
+ (link przez ``object.get_absolute_url``).
+ """
+
+ max_previous_ops = 10
+
def get_queryset(self):
# Multi-hosted: importy pokazuj tylko na stronie uczelni, z której
# zostały zaczęte (uczelnia z requestu: domena→Site→Uczelnia). Stare
@@ -66,7 +83,9 @@ def get_queryset(self):
# importy uczelni Y właściciela.
from bpp.models import Uczelnia
- qset = RestrictToOwnerMixin.get_queryset(self).order_by("-last_updated_on")
+ qset = self.model.objects.filter(owner=self.request.user).order_by(
+ "-created_on"
+ )
uczelnia = Uczelnia.objects.get_for_request(self.request)
if uczelnia is not None:
@@ -78,7 +97,7 @@ def get_queryset(self):
return qset
-class UtworzImportPlikuPolon(BaseImportPlikuPolonMixin, CreateLongRunningOperationView):
+class UtworzImportPlikuPolon(BaseImportPlikuPolonMixin, CreateLiveOperationView):
form_class = NowyImportForm
def form_valid(self, form):
@@ -86,32 +105,59 @@ def form_valid(self, form):
# Uczelnia). Zawęża późniejszą walidację ZATRUDNIENIE, dopasowanie
# autora i raport niezmatchowanych do tej jednej uczelni. Gdy nie da
# się rozstrzygnąć (single-host bez domeny / pusta baza) → None →
- # zachowanie wsteczne bez zawężenia.
+ # zachowanie wsteczne bez zawężenia. Owner + save + enqueue + redirect
+ # na get_absolute_url robi bazowe CreateLiveOperationView.form_valid.
from bpp.models import Uczelnia
form.instance.uczelnia = Uczelnia.objects.get_for_request(self.request)
return super().form_valid(form)
-class ImportPolonRouterView(BaseImportPlikuPolonMixin, LongRunningRouterView):
- redirect_prefix = "import_polon:importplikuabsencji"
+class _PkOwnerRestartMixin(GroupRequiredMixin, RestartView):
+ """Gejtowany restart importu (POLON/absencji).
+ URL restartu ma tylko ``pk`` (bez ``op_type``), więc nadpisujemy
+ ``OpTypeObjectMixin.get_object`` i rozwiązujemy konkretny model wprost,
+ owner-scoped.
-class ImportPolonDetailsView(BaseImportPlikuPolonMixin, LongRunningDetailsView):
- pass
+ Bramka grupy (precedens #508 F4): liveops ``BaseLiveOperationMixin`` gejtuje
+ tylko gdy ``LIVEOPS["REQUIRED_GROUP"]`` ustawione — w BPP NIE jest, więc
+ tamta bramka to no-op. Dokładamy braces ``GroupRequiredMixin`` (konwencja
+ projektu, jak reszta widoków importu). Restart z ``zapisz_zmiany_do_bazy=
+ True`` PONOWNIE zapisuje do bazy (akcja destrukcyjna) — nie zostawiamy go
+ owner-only.
+ """
+ model = ImportPlikuPolon
+ group_required = "wprowadzanie danych"
-class RestartImportView(BaseImportPlikuPolonMixin, RestartLongRunningOperationView):
- pass
+ def get_object(self, queryset=None):
+ return get_object_or_404(
+ self.model, pk=self.kwargs["pk"], owner=self.request.user
+ )
+
+
+class RestartImportView(_PkOwnerRestartMixin):
+ model = ImportPlikuPolon
class ZapiszDoBazyImportView(BaseImportPlikuPolonMixin, ZapiszDoBazyMixin):
pass
-class ImportPolonResultsView(BaseImportPlikuPolonMixin, LongRunningResultsView):
+class ImportPolonResultsView(BaseImportPlikuPolonMixin, ListView):
+ # paginate_by OBOWIĄZKOWE: szablon renderuje stronicowanie, a bez tego
+ # tysiące wierszy trafiłoby na jedną stronę (cicha utrata paginacji).
+ paginate_by = 25
+
+ @cached_property
+ def parent_object(self):
+ return get_object_or_404(
+ ImportPlikuPolon, pk=self.kwargs["pk"], owner=self.request.user
+ )
+
def get_queryset(self):
- queryset = super().get_queryset()
+ queryset = self.parent_object.get_details_set()
# Get filter parameters
autor_wiersz = self.request.GET.get("autor_wiersz", "").strip()
@@ -155,12 +201,12 @@ def get_queryset(self):
return queryset
def get_context_data(self, **kwargs):
- context = super().get_context_data(**kwargs)
+ # object=self.parent_object OBOWIĄZKOWE: szablon woła {% url ... object.pk %}
+ # (bez tego NoReverseMatch).
+ context = super().get_context_data(object=self.parent_object, **kwargs)
# Get base queryset for form choices
- base_queryset = ImportPlikuPolon.objects.get(
- pk=self.kwargs["pk"]
- ).get_details_set()
+ base_queryset = self.parent_object.get_details_set()
# Create filter form with current GET parameters and queryset for choices
filter_form = WierszImportuPlikuPolonFilterForm(
@@ -182,8 +228,7 @@ def get_context_data(self, **kwargs):
# realną jednostkę (obecnie lub historycznie; jednostki obce pomijane —
# patrz ImportPlikuPolon.autorzy_niezmatchowani) — bez tego raport
# wyciekałby autorów innych uczelni współistniejących w bazie.
- import_object = ImportPlikuPolon.objects.get(pk=self.kwargs["pk"])
- unmatched_autor_dyscyplina = import_object.autorzy_niezmatchowani()
+ unmatched_autor_dyscyplina = self.parent_object.autorzy_niezmatchowani()
context["unmatched_autor_dyscyplina"] = unmatched_autor_dyscyplina
context["unmatched_count"] = unmatched_autor_dyscyplina.count()