From ac49f983ee799070e3f00603eb74fd9e776f275d Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 2 Aug 2022 09:38:03 +0200 Subject: [PATCH 01/48] #185 Parcel loading on public reports * fixes bug where unauthorized clients would not load a geometries parcel view properly * minor general templates enhancements --- konova/views.py | 2 -- templates/base.html | 2 +- user/templates/user/team/index.html | 2 +- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/konova/views.py b/konova/views.py index 4fadf6a0..48572a85 100644 --- a/konova/views.py +++ b/konova/views.py @@ -102,7 +102,6 @@ def home_view(request: HttpRequest): return render(request, template, context) -@login_required def get_geom_parcels(request: HttpRequest, id: str): """ Getter for HTMX @@ -157,7 +156,6 @@ def get_geom_parcels(request: HttpRequest, id: str): return HttpResponse(None, status=404) -@login_required def get_geom_parcels_content(request: HttpRequest, id: str, page: int): """ Getter for infinite scroll of HTMX diff --git a/templates/base.html b/templates/base.html index 30be48ed..ec79ca62 100644 --- a/templates/base.html +++ b/templates/base.html @@ -31,7 +31,7 @@ {% endfor %} -
+
{% comment %} The modal wrapper, which can be used on every view can stay on the base.html template {% endcomment %} diff --git a/user/templates/user/team/index.html b/user/templates/user/team/index.html index 63560516..68b135f7 100644 --- a/user/templates/user/team/index.html +++ b/user/templates/user/team/index.html @@ -14,8 +14,8 @@ {% block body %}
-

{% trans 'Teams' %}

+

{% trans 'Teams' %}

+ {% if not has_finished_deadlines %} +
+ {% trans 'Missing finished deadline ' %} +
+ {% endif %}
diff --git a/compensation/templates/compensation/detail/eco_account/includes/deadlines.html b/compensation/templates/compensation/detail/eco_account/includes/deadlines.html index beaecfda..c0b4daf9 100644 --- a/compensation/templates/compensation/detail/eco_account/includes/deadlines.html +++ b/compensation/templates/compensation/detail/eco_account/includes/deadlines.html @@ -20,6 +20,11 @@ + {% if not has_finished_deadlines %} +
+ {% trans 'Missing finished deadline ' %} +
+ {% endif %}
diff --git a/compensation/utils/quality.py b/compensation/utils/quality.py index b622fcdd..f883ba36 100644 --- a/compensation/utils/quality.py +++ b/compensation/utils/quality.py @@ -19,6 +19,7 @@ class CompensationQualityChecker(AbstractQualityChecker): self._check_states() self._check_actions() self._check_geometry() + self._check_deadlines() self.valid = len(self.messages) == 0 def _check_states(self): @@ -47,6 +48,16 @@ class CompensationQualityChecker(AbstractQualityChecker): if not self.obj.actions.all(): self._add_missing_attr_name(_con("Compensation", "Actions")) + def _check_deadlines(self): + """ Checks data quality for related Deadline objects + + Returns: + + """ + finished_deadlines = self.obj.get_finished_deadlines() + if not finished_deadlines.exists(): + self._add_missing_attr_name(_("Finished deadlines")) + class EcoAccountQualityChecker(CompensationQualityChecker): def run_check(self): diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py index 31087ed7..efe51ce3 100644 --- a/compensation/views/compensation.py +++ b/compensation/views/compensation.py @@ -240,6 +240,7 @@ def detail_view(request: HttpRequest, id: str): "is_ets_member": in_group(_user, ETS_GROUP), "LANIS_LINK": comp.get_LANIS_link(), TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}", + "has_finished_deadlines": comp.get_finished_deadlines().exists(), } context = BaseContext(request, context).context return render(request, template, context) diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index ecaccbe6..ebface8d 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -242,6 +242,7 @@ def detail_view(request: HttpRequest, id: str): "deductions": deductions, "actions": actions, TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}", + "has_finished_deadlines": acc.get_finished_deadlines().exists(), } context = BaseContext(request, context).context return render(request, template, context) diff --git a/ema/templates/ema/detail/includes/deadlines.html b/ema/templates/ema/detail/includes/deadlines.html index 761ce067..0c25b390 100644 --- a/ema/templates/ema/detail/includes/deadlines.html +++ b/ema/templates/ema/detail/includes/deadlines.html @@ -20,6 +20,11 @@ + {% if not has_finished_deadlines %} +
+ {% trans 'Missing finished deadline ' %} +
+ {% endif %}
diff --git a/ema/views.py b/ema/views.py index ce0d68f5..589165f5 100644 --- a/ema/views.py +++ b/ema/views.py @@ -166,6 +166,7 @@ def detail_view(request: HttpRequest, id: str): "is_ets_member": in_group(_user, ETS_GROUP), "LANIS_LINK": ema.get_LANIS_link(), TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}", + "has_finished_deadlines": ema.get_finished_deadlines().exists(), } context = BaseContext(request, context).context return render(request, template, context) diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index 4037c6bf..c6006190 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -22,7 +22,7 @@ from codelist.models import KonovaCode, KonovaCodeList from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount, EcoAccountDeduction from intervention.models import Legal, Responsibility, Intervention, Handler from konova.management.commands.setup_data import GROUPS_DATA -from konova.models import Geometry +from konova.models import Geometry, Deadline, DeadlineType from konova.settings import DEFAULT_GROUP from konova.utils.generators import generate_random_string from user.models import UserActionLogEntry @@ -41,6 +41,7 @@ class BaseTestCase(TestCase): eco_account = None comp_state = None comp_action = None + finished_deadline = None codes = None superuser_pw = "root" @@ -69,6 +70,7 @@ class BaseTestCase(TestCase): self.create_dummy_action() self.codes = self.create_dummy_codes() self.team = self.create_dummy_team() + self.finished_deadline = self.create_dummy_deadline() # Set the default group as only group for the user default_group = self.groups.get(name=DEFAULT_GROUP) @@ -279,6 +281,20 @@ class BaseTestCase(TestCase): return team + def create_dummy_deadline(self, type: DeadlineType = DeadlineType.FINISHED): + """ Creates a dummy deadline. + + If type is not specified, it defaults to DeadlineType.FINISHED + + Returns: + deadline (Deadline): A deadline + """ + deadline = Deadline.objects.create( + type=type, + date="1970-01-01" + ) + return deadline + @staticmethod def create_dummy_geometry() -> MultiPolygon: """ Creates some geometry @@ -361,6 +377,7 @@ class BaseTestCase(TestCase): compensation.before_states.add(self.comp_state) compensation.actions.add(self.comp_action) compensation.geometry.geom = self.create_dummy_geometry() + compensation.deadlines.add(self.finished_deadline) compensation.geometry.save() return compensation @@ -390,6 +407,7 @@ class BaseTestCase(TestCase): ema.before_states.add(self.comp_state) ema.actions.add(self.comp_action) ema.geometry.geom = self.create_dummy_geometry() + ema.deadlines.add(self.finished_deadline) ema.geometry.save() return ema @@ -410,6 +428,7 @@ class BaseTestCase(TestCase): eco_account.geometry.geom = self.create_dummy_geometry() eco_account.geometry.save() eco_account.deductable_surface = eco_account.get_state_after_surface_sum() + eco_account.deadlines.add(self.finished_deadline) eco_account.save() return eco_account diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 7072e4c2765d9290ef6c1c9cd3971d3b72a6cf1e..5b2d55a9dbb4a98c46dd7d7eb3f0612c61cd9508 100644 GIT binary patch delta 12151 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 delta 12043 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 lLe diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index c62c2bd3..1da188a2 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -26,7 +26,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-06-27 14:23+0200\n" +"POT-Creation-Date: 2022-08-08 14:39+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -55,7 +55,7 @@ msgstr "Einträge erstellt bis..." #: analysis/forms.py:49 compensation/forms/forms.py:77 #: compensation/templates/compensation/detail/eco_account/view.html:59 #: compensation/templates/compensation/report/eco_account/report.html:16 -#: compensation/utils/quality.py:100 ema/templates/ema/detail/view.html:49 +#: compensation/utils/quality.py:113 ema/templates/ema/detail/view.html:49 #: ema/templates/ema/report/report.html:16 ema/utils/quality.py:26 #: intervention/forms/forms.py:102 #: intervention/templates/intervention/detail/view.html:56 @@ -296,7 +296,7 @@ msgid "Law" msgstr "Gesetz" #: analysis/templates/analysis/reports/includes/old_data/amount.html:17 -#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:28 +#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:33 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:28 #: ema/templates/ema/detail/includes/deadlines.html:28 msgid "Type" @@ -375,7 +375,7 @@ msgstr "Kompensation XY; Flur ABC" #: compensation/forms/forms.py:57 compensation/forms/modalForms.py:63 #: compensation/forms/modalForms.py:361 compensation/forms/modalForms.py:469 #: compensation/templates/compensation/detail/compensation/includes/actions.html:35 -#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:34 +#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:39 #: compensation/templates/compensation/detail/compensation/includes/documents.html:34 #: compensation/templates/compensation/detail/eco_account/includes/actions.html:34 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:34 @@ -399,7 +399,7 @@ msgstr "Zusätzlicher Kommentar" #: compensation/forms/forms.py:93 #: compensation/templates/compensation/detail/eco_account/view.html:63 #: compensation/templates/compensation/report/eco_account/report.html:20 -#: compensation/utils/quality.py:102 ema/templates/ema/detail/view.html:53 +#: compensation/utils/quality.py:115 ema/templates/ema/detail/view.html:53 #: ema/templates/ema/report/report.html:20 ema/utils/quality.py:28 #: intervention/forms/forms.py:130 #: intervention/templates/intervention/detail/view.html:60 @@ -485,7 +485,7 @@ msgstr "Neue Kompensation" msgid "Edit compensation" msgstr "Bearbeite Kompensation" -#: compensation/forms/forms.py:356 compensation/utils/quality.py:84 +#: compensation/forms/forms.py:356 compensation/utils/quality.py:97 msgid "Available Surface" msgstr "Verfügbare Fläche" @@ -495,7 +495,7 @@ msgstr "Die für Abbuchungen zur Verfügung stehende Menge" #: compensation/forms/forms.py:368 #: compensation/templates/compensation/detail/eco_account/view.html:67 -#: compensation/utils/quality.py:72 +#: compensation/utils/quality.py:85 msgid "Agreement date" msgstr "Vereinbarungsdatum" @@ -597,7 +597,7 @@ msgid "Select the deadline type" msgstr "Fristart wählen" #: compensation/forms/modalForms.py:345 -#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:31 +#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:36 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:31 #: ema/templates/ema/detail/includes/deadlines.html:31 #: intervention/forms/modalForms.py:149 @@ -617,7 +617,7 @@ msgid "Insert data for the new deadline" msgstr "Geben Sie die Daten der neuen Frist ein" #: compensation/forms/modalForms.py:389 -#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:59 +#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:64 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:57 #: ema/templates/ema/detail/includes/deadlines.html:57 msgid "Edit deadline" @@ -798,7 +798,7 @@ msgid "Amount" msgstr "Menge" #: compensation/templates/compensation/detail/compensation/includes/actions.html:40 -#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:39 +#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:44 #: compensation/templates/compensation/detail/compensation/includes/documents.html:39 #: compensation/templates/compensation/detail/compensation/includes/states-after.html:41 #: compensation/templates/compensation/detail/compensation/includes/states-before.html:41 @@ -882,7 +882,11 @@ msgstr "Termine und Fristen" msgid "Add new deadline" msgstr "Frist/Termin hinzufügen" -#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:62 +#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:25 +msgid "Missing finished deadline " +msgstr "Umsetzungstermin fehlt" + +#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:67 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:60 #: ema/templates/ema/detail/includes/deadlines.html:60 msgid "Remove deadline" @@ -928,7 +932,7 @@ msgstr "Dokument löschen" #: compensation/templates/compensation/detail/compensation/includes/states-after.html:8 #: compensation/templates/compensation/detail/eco_account/includes/states-after.html:8 -#: compensation/utils/quality.py:39 +#: compensation/utils/quality.py:42 #: ema/templates/ema/detail/includes/states-after.html:8 msgid "States after" msgstr "Zielzustand" @@ -974,7 +978,7 @@ msgstr "Zustand entfernen" #: compensation/templates/compensation/detail/compensation/includes/states-before.html:8 #: compensation/templates/compensation/detail/eco_account/includes/states-before.html:8 -#: compensation/utils/quality.py:37 +#: compensation/utils/quality.py:40 #: ema/templates/ema/detail/includes/states-before.html:8 msgid "States before" msgstr "Ausgangszustand" @@ -1067,6 +1071,24 @@ msgstr "Zuletzt bearbeitet" msgid "Shared with" msgstr "Freigegeben für" +#: compensation/templates/compensation/detail/compensation/view.html:132 +#: compensation/templates/compensation/detail/eco_account/view.html:110 +#: ema/templates/ema/detail/view.html:96 +#: intervention/templates/intervention/detail/view.html:138 +msgid "" +"The data must be shared with you, if you want to see which other users have " +"shared access as well." +msgstr "" +"Die Daten müssen für Sie freigegeben sein, damit Sie sehen können welche " +"weiteren Nutzern ebenfalls Zugriff hierauf haben." + +#: compensation/templates/compensation/detail/compensation/view.html:134 +#: compensation/templates/compensation/detail/eco_account/view.html:112 +#: ema/templates/ema/detail/view.html:98 +#: intervention/templates/intervention/detail/view.html:140 +msgid "other users" +msgstr "weitere Nutzer" + #: compensation/templates/compensation/detail/eco_account/includes/controls.html:15 #: ema/templates/ema/detail/includes/controls.html:15 #: intervention/forms/modalForms.py:71 @@ -1166,21 +1188,25 @@ msgstr "Abbuchungen für" msgid "None" msgstr "-" -#: compensation/utils/quality.py:34 +#: compensation/utils/quality.py:37 msgid "States unequal" msgstr "Ungleiche Zustandsflächenmengen" -#: compensation/utils/quality.py:74 intervention/utils/quality.py:84 +#: compensation/utils/quality.py:61 +msgid "Finished deadlines" +msgstr "Umsetzungstermin" + +#: compensation/utils/quality.py:87 intervention/utils/quality.py:84 msgid "Legal data" msgstr "Rechtliche Daten" -#: compensation/utils/quality.py:88 +#: compensation/utils/quality.py:101 msgid "Deductable surface can not be larger than state surface" msgstr "" "Die abbuchbare Fläche darf die Gesamtfläche der Zielzustände nicht " "überschreiten" -#: compensation/utils/quality.py:104 ema/utils/quality.py:30 +#: compensation/utils/quality.py:117 ema/utils/quality.py:30 #: intervention/utils/quality.py:55 msgid "Responsible data" msgstr "Daten zu den verantwortlichen Stellen" @@ -1198,12 +1224,12 @@ msgstr "Kompensation {} bearbeitet" msgid "Edit {}" msgstr "Bearbeite {}" -#: compensation/views/compensation.py:268 compensation/views/eco_account.py:359 +#: compensation/views/compensation.py:269 compensation/views/eco_account.py:359 #: ema/views.py:194 intervention/views.py:542 msgid "Log" msgstr "Log" -#: compensation/views/compensation.py:612 compensation/views/eco_account.py:727 +#: compensation/views/compensation.py:613 compensation/views/eco_account.py:727 #: ema/views.py:558 intervention/views.py:688 msgid "Report {}" msgstr "Bericht {}" @@ -1549,18 +1575,6 @@ msgstr "Eingriffsverursacher" msgid "Exists" msgstr "vorhanden" -#: intervention/templates/intervention/detail/view.html:138 -msgid "" -"The data must be shared with you, if you want to see which other users have " -"shared access as well." -msgstr "" -"Die Daten müssen für Sie freigegeben sein, damit Sie sehen können welche weiteren Nutzern " -"ebenfalls Zugriff hierauf haben." - -#: intervention/templates/intervention/detail/view.html:140 -msgid "other users" -msgstr "weitere Nutzer" - #: intervention/templates/intervention/report/report.html:58 msgid "Deductions of eco-accounts" msgstr "Abbuchungen von Ökokonten" @@ -2753,7 +2767,7 @@ msgstr "Benachrichtigungen" msgid "Manage teams" msgstr "" -#: user/templates/user/index.html:61 user/templates/user/team/index.html:18 +#: user/templates/user/index.html:61 user/templates/user/team/index.html:19 #: user/views.py:167 msgid "Teams" msgstr "" From 4138481a1b2276696b378a4af424e20c4baf7f62 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 10 Aug 2022 08:03:18 +0200 Subject: [PATCH 03/48] New Notification * adds new notification setting to user settings form * adds translations * adds initial creating of ENUM on setup command --- konova/management/commands/setup_data.py | 3 +- locale/de/LC_MESSAGES/django.mo | Bin 44074 -> 44182 bytes locale/de/LC_MESSAGES/django.po | 82 ++++++++++++----------- user/enums.py | 3 +- user/forms.py | 2 +- 5 files changed, 49 insertions(+), 41 deletions(-) diff --git a/konova/management/commands/setup_data.py b/konova/management/commands/setup_data.py index 4a5683cd..078b547e 100644 --- a/konova/management/commands/setup_data.py +++ b/konova/management/commands/setup_data.py @@ -28,4 +28,5 @@ USER_NOTIFICATIONS_NAMES = { "NOTIFY_ON_SHARED_DATA_RECORDED": _("On shared data recorded"), "NOTIFY_ON_SHARED_DATA_DELETED": _("On shared data deleted"), "NOTIFY_ON_SHARED_DATA_CHECKED": _("On shared data checked"), -} \ No newline at end of file + "NOTIFY_ON_DEDUCTION_CHANGES": _("On deduction changes"), +} diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 5b2d55a9dbb4a98c46dd7d7eb3f0612c61cd9508..d1cbdfba463cad7b0e3308a74702bbfb3158c120 100644 GIT binary patch delta 11374 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 diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 1da188a2..893d4384 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -26,7 +26,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-08-08 14:39+0200\n" +"POT-Creation-Date: 2022-08-10 08:01+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -55,7 +55,7 @@ msgstr "Einträge erstellt bis..." #: analysis/forms.py:49 compensation/forms/forms.py:77 #: compensation/templates/compensation/detail/eco_account/view.html:59 #: compensation/templates/compensation/report/eco_account/report.html:16 -#: compensation/utils/quality.py:113 ema/templates/ema/detail/view.html:49 +#: compensation/utils/quality.py:111 ema/templates/ema/detail/view.html:49 #: ema/templates/ema/report/report.html:16 ema/utils/quality.py:26 #: intervention/forms/forms.py:102 #: intervention/templates/intervention/detail/view.html:56 @@ -297,8 +297,8 @@ msgstr "Gesetz" #: analysis/templates/analysis/reports/includes/old_data/amount.html:17 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:33 -#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:28 -#: ema/templates/ema/detail/includes/deadlines.html:28 +#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:33 +#: ema/templates/ema/detail/includes/deadlines.html:33 msgid "Type" msgstr "Typ" @@ -378,10 +378,10 @@ msgstr "Kompensation XY; Flur ABC" #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:39 #: compensation/templates/compensation/detail/compensation/includes/documents.html:34 #: compensation/templates/compensation/detail/eco_account/includes/actions.html:34 -#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:34 +#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:39 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:34 #: ema/templates/ema/detail/includes/actions.html:34 -#: ema/templates/ema/detail/includes/deadlines.html:34 +#: ema/templates/ema/detail/includes/deadlines.html:39 #: ema/templates/ema/detail/includes/documents.html:34 #: intervention/forms/forms.py:198 intervention/forms/modalForms.py:175 #: intervention/templates/intervention/detail/includes/documents.html:34 @@ -399,7 +399,7 @@ msgstr "Zusätzlicher Kommentar" #: compensation/forms/forms.py:93 #: compensation/templates/compensation/detail/eco_account/view.html:63 #: compensation/templates/compensation/report/eco_account/report.html:20 -#: compensation/utils/quality.py:115 ema/templates/ema/detail/view.html:53 +#: compensation/utils/quality.py:113 ema/templates/ema/detail/view.html:53 #: ema/templates/ema/report/report.html:20 ema/utils/quality.py:28 #: intervention/forms/forms.py:130 #: intervention/templates/intervention/detail/view.html:60 @@ -485,7 +485,7 @@ msgstr "Neue Kompensation" msgid "Edit compensation" msgstr "Bearbeite Kompensation" -#: compensation/forms/forms.py:356 compensation/utils/quality.py:97 +#: compensation/forms/forms.py:356 compensation/utils/quality.py:95 msgid "Available Surface" msgstr "Verfügbare Fläche" @@ -495,7 +495,7 @@ msgstr "Die für Abbuchungen zur Verfügung stehende Menge" #: compensation/forms/forms.py:368 #: compensation/templates/compensation/detail/eco_account/view.html:67 -#: compensation/utils/quality.py:85 +#: compensation/utils/quality.py:83 msgid "Agreement date" msgstr "Vereinbarungsdatum" @@ -598,8 +598,8 @@ msgstr "Fristart wählen" #: compensation/forms/modalForms.py:345 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:36 -#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:31 -#: ema/templates/ema/detail/includes/deadlines.html:31 +#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:36 +#: ema/templates/ema/detail/includes/deadlines.html:36 #: intervention/forms/modalForms.py:149 msgid "Date" msgstr "Datum" @@ -618,8 +618,8 @@ msgstr "Geben Sie die Daten der neuen Frist ein" #: compensation/forms/modalForms.py:389 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:64 -#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:57 -#: ema/templates/ema/detail/includes/deadlines.html:57 +#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:62 +#: ema/templates/ema/detail/includes/deadlines.html:62 msgid "Edit deadline" msgstr "Frist/Termin bearbeiten" @@ -803,13 +803,13 @@ msgstr "Menge" #: compensation/templates/compensation/detail/compensation/includes/states-after.html:41 #: compensation/templates/compensation/detail/compensation/includes/states-before.html:41 #: compensation/templates/compensation/detail/eco_account/includes/actions.html:39 -#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:38 +#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:43 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:41 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:38 #: compensation/templates/compensation/detail/eco_account/includes/states-after.html:41 #: compensation/templates/compensation/detail/eco_account/includes/states-before.html:41 #: ema/templates/ema/detail/includes/actions.html:38 -#: ema/templates/ema/detail/includes/deadlines.html:38 +#: ema/templates/ema/detail/includes/deadlines.html:43 #: ema/templates/ema/detail/includes/documents.html:38 #: ema/templates/ema/detail/includes/states-after.html:40 #: ema/templates/ema/detail/includes/states-before.html:40 @@ -883,12 +883,14 @@ msgid "Add new deadline" msgstr "Frist/Termin hinzufügen" #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:25 +#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:25 +#: ema/templates/ema/detail/includes/deadlines.html:25 msgid "Missing finished deadline " msgstr "Umsetzungstermin fehlt" #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:67 -#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:60 -#: ema/templates/ema/detail/includes/deadlines.html:60 +#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:65 +#: ema/templates/ema/detail/includes/deadlines.html:65 msgid "Remove deadline" msgstr "Frist löschen" @@ -932,7 +934,7 @@ msgstr "Dokument löschen" #: compensation/templates/compensation/detail/compensation/includes/states-after.html:8 #: compensation/templates/compensation/detail/eco_account/includes/states-after.html:8 -#: compensation/utils/quality.py:42 +#: compensation/utils/quality.py:40 #: ema/templates/ema/detail/includes/states-after.html:8 msgid "States after" msgstr "Zielzustand" @@ -978,7 +980,7 @@ msgstr "Zustand entfernen" #: compensation/templates/compensation/detail/compensation/includes/states-before.html:8 #: compensation/templates/compensation/detail/eco_account/includes/states-before.html:8 -#: compensation/utils/quality.py:40 +#: compensation/utils/quality.py:38 #: ema/templates/ema/detail/includes/states-before.html:8 msgid "States before" msgstr "Ausgangszustand" @@ -1188,25 +1190,25 @@ msgstr "Abbuchungen für" msgid "None" msgstr "-" -#: compensation/utils/quality.py:37 +#: compensation/utils/quality.py:35 msgid "States unequal" msgstr "Ungleiche Zustandsflächenmengen" -#: compensation/utils/quality.py:61 +#: compensation/utils/quality.py:59 msgid "Finished deadlines" msgstr "Umsetzungstermin" -#: compensation/utils/quality.py:87 intervention/utils/quality.py:84 +#: compensation/utils/quality.py:85 intervention/utils/quality.py:84 msgid "Legal data" msgstr "Rechtliche Daten" -#: compensation/utils/quality.py:101 +#: compensation/utils/quality.py:99 msgid "Deductable surface can not be larger than state surface" msgstr "" "Die abbuchbare Fläche darf die Gesamtfläche der Zielzustände nicht " "überschreiten" -#: compensation/utils/quality.py:117 ema/utils/quality.py:30 +#: compensation/utils/quality.py:115 ema/utils/quality.py:30 #: intervention/utils/quality.py:55 msgid "Responsible data" msgstr "Daten zu den verantwortlichen Stellen" @@ -1220,17 +1222,17 @@ msgid "Compensation {} edited" msgstr "Kompensation {} bearbeitet" #: compensation/views/compensation.py:182 compensation/views/eco_account.py:173 -#: ema/views.py:240 intervention/views.py:338 +#: ema/views.py:241 intervention/views.py:338 msgid "Edit {}" msgstr "Bearbeite {}" -#: compensation/views/compensation.py:269 compensation/views/eco_account.py:359 -#: ema/views.py:194 intervention/views.py:542 +#: compensation/views/compensation.py:269 compensation/views/eco_account.py:360 +#: ema/views.py:195 intervention/views.py:542 msgid "Log" msgstr "Log" -#: compensation/views/compensation.py:613 compensation/views/eco_account.py:727 -#: ema/views.py:558 intervention/views.py:688 +#: compensation/views/compensation.py:613 compensation/views/eco_account.py:728 +#: ema/views.py:559 intervention/views.py:688 msgid "Report {}" msgstr "Bericht {}" @@ -1246,36 +1248,36 @@ msgstr "Ökokonto {} hinzugefügt" msgid "Eco-Account {} edited" msgstr "Ökokonto {} bearbeitet" -#: compensation/views/eco_account.py:276 +#: compensation/views/eco_account.py:277 msgid "Eco-account removed" msgstr "Ökokonto entfernt" -#: compensation/views/eco_account.py:380 ema/views.py:282 +#: compensation/views/eco_account.py:381 ema/views.py:283 #: intervention/views.py:641 msgid "{} unrecorded" msgstr "{} entzeichnet" -#: compensation/views/eco_account.py:380 ema/views.py:282 +#: compensation/views/eco_account.py:381 ema/views.py:283 #: intervention/views.py:641 msgid "{} recorded" msgstr "{} verzeichnet" -#: compensation/views/eco_account.py:804 ema/views.py:628 +#: compensation/views/eco_account.py:805 ema/views.py:629 #: intervention/views.py:439 msgid "{} has already been shared with you" msgstr "{} wurde bereits für Sie freigegeben" -#: compensation/views/eco_account.py:809 ema/views.py:633 +#: compensation/views/eco_account.py:810 ema/views.py:634 #: intervention/views.py:444 msgid "{} has been shared with you" msgstr "{} ist nun für Sie freigegeben" -#: compensation/views/eco_account.py:816 ema/views.py:640 +#: compensation/views/eco_account.py:817 ema/views.py:641 #: intervention/views.py:451 msgid "Share link invalid" msgstr "Freigabelink ungültig" -#: compensation/views/eco_account.py:839 ema/views.py:663 +#: compensation/views/eco_account.py:840 ema/views.py:664 #: intervention/views.py:474 msgid "Share settings updated" msgstr "Freigabe Einstellungen aktualisiert" @@ -1316,11 +1318,11 @@ msgstr "EMAs - Übersicht" msgid "EMA {} added" msgstr "EMA {} hinzugefügt" -#: ema/views.py:230 +#: ema/views.py:231 msgid "EMA {} edited" msgstr "EMA {} bearbeitet" -#: ema/views.py:263 +#: ema/views.py:264 msgid "EMA removed" msgstr "EMA entfernt" @@ -1799,6 +1801,10 @@ msgstr "Wenn meine freigegebenen Daten gelöscht wurden" msgid "On shared data checked" msgstr "Wenn meine freigegebenen Daten geprüft wurden" +#: konova/management/commands/setup_data.py:31 +msgid "On deduction changes" +msgstr "Wenn eine Abbuchung zu meinem Ökokonto verändert oder gelöscht wird" + #: konova/models/deadline.py:18 msgid "Finished" msgstr "Umgesetzt bis" diff --git a/user/enums.py b/user/enums.py index 8d0c34de..04f27b77 100644 --- a/user/enums.py +++ b/user/enums.py @@ -13,4 +13,5 @@ class UserNotificationEnum(BaseEnum): NOTIFY_ON_SHARED_DATA_RECORDED = "NOTIFY_ON_SHARED_DATA_RECORDED" # notifies in case data has been "verzeichnet" NOTIFY_ON_SHARED_DATA_DELETED = "NOTIFY_ON_SHARED_DATA_DELETED" # notifies in case data has been deleted NOTIFY_ON_SHARED_DATA_CHECKED = "NOTIFY_ON_SHARED_DATA_CHECKED" # notifies in case shared data has been checked - NOTIFY_ON_SHARED_ACCESS_GAINED = "NOTIFY_ON_SHARED_ACCESS_GAINED" # notifies in case new access has been gained \ No newline at end of file + NOTIFY_ON_SHARED_ACCESS_GAINED = "NOTIFY_ON_SHARED_ACCESS_GAINED" # notifies in case new access has been gained + NOTIFY_ON_DEDUCTION_CHANGES = "NOTIFY_ON_DEDUCTION_CHANGES" # notifies in case any changes (edit|remove) have been performed on a deduction of the user's ecoaccounts \ No newline at end of file diff --git a/user/forms.py b/user/forms.py index a92c6b00..12688b46 100644 --- a/user/forms.py +++ b/user/forms.py @@ -7,7 +7,7 @@ Created on: 08.07.21 """ from dal import autocomplete from django import forms -from django.db import IntegrityError, transaction +from django.db import transaction from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ From ff26019b6efddaf4a90455467a14c83fb25994b5 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 10 Aug 2022 08:59:24 +0200 Subject: [PATCH 04/48] Mail sending * adds mail sending logic for new notification setting * adds new templates for user and team based sending * enhances all email template layout * adds translations --- compensation/models/eco_account.py | 22 ++++++- intervention/forms/modalForms.py | 34 ++++++++--- konova/tasks.py | 14 +++++ konova/utils/mailer.py | 55 ++++++++++++++++++ locale/de/LC_MESSAGES/django.mo | Bin 44182 -> 44532 bytes locale/de/LC_MESSAGES/django.po | 54 +++++++++++++---- templates/email/api/verify_token.html | 1 + .../email/checking/shared_data_checked.html | 1 + .../checking/shared_data_checked_team.html | 1 + .../email/deleting/shared_data_deleted.html | 1 + .../deleting/shared_data_deleted_team.html | 1 + templates/email/other/deduction_changed.html | 50 ++++++++++++++++ .../email/other/deduction_changed_team.html | 50 ++++++++++++++++ .../email/recording/shared_data_recorded.html | 1 + .../recording/shared_data_recorded_team.html | 1 + .../recording/shared_data_unrecorded.html | 1 + .../shared_data_unrecorded_team.html | 1 + .../email/sharing/shared_access_given.html | 1 + .../sharing/shared_access_given_team.html | 1 + .../email/sharing/shared_access_removed.html | 1 + .../sharing/shared_access_removed_team.html | 1 + user/models/team.py | 14 +++++ user/models/user.py | 16 +++++ 23 files changed, 300 insertions(+), 22 deletions(-) create mode 100644 templates/email/other/deduction_changed.html create mode 100644 templates/email/other/deduction_changed_team.html diff --git a/compensation/models/eco_account.py b/compensation/models/eco_account.py index 3d48b691..42a202f7 100644 --- a/compensation/models/eco_account.py +++ b/compensation/models/eco_account.py @@ -21,6 +21,7 @@ from compensation.models.compensation import AbstractCompensation, PikMixin from compensation.utils.quality import EcoAccountQualityChecker from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \ generate_document_file_upload_path +from konova.tasks import celery_send_mail_deduction_changed, celery_send_mail_deduction_changed_team class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin): @@ -161,6 +162,25 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix """ return reverse("compensation:acc:share", args=(self.id, self.access_token)) + def send_notification_mail_on_deduction_change(self, data_change: dict): + """ Sends notification mails for changes on the deduction + + Args: + data_change (): + + Returns: + + """ + # Send mail + shared_users = self.shared_users.values_list("id", flat=True) + for user_id in shared_users: + celery_send_mail_deduction_changed.delay(self.identifier, self.title, user_id, data_change) + + # Send mail + shared_teams = self.shared_teams.values_list("id", flat=True) + for team_id in shared_teams: + celery_send_mail_deduction_changed_team.delay(self.identifier, self.title, team_id, data_change) + class EcoAccountDocument(AbstractDocument): """ @@ -251,4 +271,4 @@ class EcoAccountDeduction(BaseResource): if user is not None: self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED) self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED) - super().delete(*args, **kwargs) \ No newline at end of file + super().delete(*args, **kwargs) diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index 07911be9..b6445a55 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -508,28 +508,44 @@ class EditEcoAccountDeductionModalForm(NewDeductionModalForm): deduction = self.deduction form_account = self.cleaned_data.get("account", None) form_intervention = self.cleaned_data.get("intervention", None) - current_account = deduction.account - current_intervention = deduction.intervention - + old_account = deduction.account + old_intervention = deduction.intervention + old_surface = deduction.surface # If account or intervention has been changed, we put that change in the logs just as if the deduction has # been removed for this entry. Act as if the deduction is newly created for the new entries - if current_account != form_account: - current_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED) + if old_account != form_account: + old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED) form_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED) else: - current_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED) + old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED) - if current_intervention != form_intervention: - current_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED) + if old_intervention != form_intervention: + old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED) form_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED) else: - current_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED) + old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED) deduction.account = form_account deduction.intervention = self.cleaned_data.get("intervention", None) deduction.surface = self.cleaned_data.get("surface", None) deduction.save() + + data_changes = { + "surface": { + "old": old_surface, + "new": deduction.surface, + }, + "intervention": { + "old": old_intervention.identifier, + "new": deduction.intervention.identifier, + }, + "account": { + "old": old_account.identifier, + "new": deduction.account.identifier, + } + } + old_account.send_notification_mail_on_deduction_change(data_changes) return deduction diff --git a/konova/tasks.py b/konova/tasks.py index 798effb4..65801219 100644 --- a/konova/tasks.py +++ b/konova/tasks.py @@ -106,3 +106,17 @@ def celery_send_mail_shared_data_checked_team(obj_identifier, obj_title=None, te from user.models import Team team = Team.objects.get(id=team_id) team.send_mail_shared_data_checked(obj_identifier, obj_title) + + +@shared_task +def celery_send_mail_deduction_changed_team(obj_identifier, obj_title=None, team_id=None, data_changes=None): + from user.models import Team + team = Team.objects.get(id=team_id) + team.send_mail_deduction_changed(obj_identifier, obj_title, data_changes) + + +@shared_task +def celery_send_mail_deduction_changed(obj_identifier, obj_title=None, user_id=None, data_changes=None): + from user.models import User + user = User.objects.get(id=user_id) + user.send_mail_deduction_changed(obj_identifier, obj_title, data_changes) diff --git a/konova/utils/mailer.py b/konova/utils/mailer.py index dd8eef1f..92bd2b60 100644 --- a/konova/utils/mailer.py +++ b/konova/utils/mailer.py @@ -207,6 +207,33 @@ class Mailer: msg ) + def send_mail_deduction_changed_team(self, obj_identifier, obj_title, team, data_changes): + """ Send a mail if deduction has been changed + + Args: + obj_identifier (str): Identifier of the main object + obj_title (str): Title of the main object + team (Team): Team to be notified + data_changes (dict): Contains the old|new changes of the deduction changes + + Returns: + + """ + context = { + "team": team, + "obj_identifier": obj_identifier, + "obj_title": obj_title, + "EMAIL_REPLY_TO": EMAIL_REPLY_TO, + "data_changes": data_changes, + } + msg = render_to_string("email/other/deduction_changed_team.html", context) + user_mail_address = team.users.values_list("email", flat=True) + self.send( + user_mail_address, + _("{} - Deduction changed").format(obj_identifier), + msg + ) + def send_mail_shared_data_deleted_team(self, obj_identifier, obj_title, team): """ Send a mail if data has just been deleted @@ -322,6 +349,34 @@ class Mailer: msg ) + def send_mail_deduction_changed(self, obj_identifier, obj_title, user, data_changes): + """ Send a mail if deduction has been changed + + Args: + obj_identifier (str): Identifier of the main object + obj_title (str): Title of the main object + user (User): User to be notified + data_changes (dict): Contains the old|new changes of the deduction changes + + + Returns: + + """ + context = { + "user": user, + "obj_identifier": obj_identifier, + "obj_title": obj_title, + "EMAIL_REPLY_TO": EMAIL_REPLY_TO, + "data_changes": data_changes, + } + msg = render_to_string("email/other/deduction_changed.html", context) + user_mail_address = [user.email] + self.send( + user_mail_address, + _("{} - Deduction changed").format(obj_identifier), + msg + ) + def send_mail_verify_api_token(self, user): """ Send a mail if a user creates a new token diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index d1cbdfba463cad7b0e3308a74702bbfb3158c120..6ee76a29ae6b97210a548c54b5f64f6f34f30f49 100644 GIT binary patch delta 12475 zcmZA72YgT0|HttwOAYSWg`)}~5- zwK`~PmDa4%8ZEW@^LxE>PWtcTe;@ti`8ns_d(OG%ocql;9$j$O>(BFE?!^K=^Bs;_ zUXD{7kB2zU1M+cEs&$-pRU9XOF2`w)nUwofb)3AEAK=%Ny{kJ;7d(wASg{7z;$lp| z+c+8P)pVQ)Jb<x9KR&NM1YVFBETd2z2TA45OgfO+tmt-psf;P}wmC=5Z} zHy!ig0_z&oecMsbbimeM#ZdZp{vZkFgnw-s!6K+~Ma+-&u^_g=P)tO1oPp|S5*EU_ z*7aC~@_r1*v#9ItTXWSh4;qYR=--JViNRK=Cmn;jVFG&N9Ax&LOnd$iZlHV=HL&!$ zW%(bpJ;mr*nD2sQBh_07zb#c;~8sD2XaGym$K3l&=PWK_eWuqe*NNL+*J z=rHQWE2shAvgJQf_dP=mB=`k0L#0qNQXbWQUDW-pP&3@YB`HqQ6Sa0@QByh{eeh${ zOzgJjPof5V0oCv=RKrhEYaP_U46G2grW}dvJ7);a!quqzsx&kMbsLhXf%dkdFKT4z zsE%CJOw2}&d;@Cjx7zZjs5L!+#qc<4W`02}**#nSFKTIWH!|%;AOmroN+cS2eN@BE zP*eC4YNWkU9i*Zfd<`{I(^2Oapw@aV>ik~Rj2=Y|{03^xy&9XnQw%kbsu-mAzZr=- z=!j~lHlQSA;yJy1Go;8U&hR8RlT zDiW>ncGMGmfqJsjsE)3pX6O!T2_B<5%Jrg|u|U)SLy-TRXnttd4?;cpL~MbJQ3JVx zzW4xLP5Dz2HR#pEtYIGXp+8Ka8n1UKm8tQ?jS?8f1 zY$a-dTbnTdYVb=cO5wMth901%>KSSV0-KtK!>mP-4?iao+hbSc_ldI+b^RsO+W(5f z(YKlTr85eeOf!tE9gPJ zHDhrkKEVJS*2c`hXjF$&(1R;b?H;t}52FTt3iGR_^CW>(+(M1~AqHXYmrTQgx9+-;SGfS}v?y%)2sJ&7kf&26%_zp*n1WwXiel zhBq-Uu0rjNk5Ef=81vy-)RX;!dg8mNss0nyt{NT^=c3N9L=EH^@(a;9g{-0z+}^yd-BIVav}gW(Np?`7 z*Jw9t?Z3l(cmdVHEz}G>#$fd8VCoBF9OYBEprz4+sSO(K@G_H52zy9eQ^$*N32yH|cn?|ArUDPI-ikiZ=QA@H2)zLQVA=KM)8r9JyTYm@Df%CHYge!pRAljCzq1tbP z+MEf2^p+UR#dJLKDylY)BU%~%!pU~~KpwWhJ%%qL_6 z)C@Jj0@xPAurKOCUbW>pSdjjmHA-+NY7d-1U3eKa(#NQdeY=~P3Pa66QB=p3Q1`{6 z_ChPvns>G30jSq919jhW)E?N1t~%IfD~_TX_`#O1+46lM8 z-vG6CFQGP7Z&U*#Q4LN&P5BI4KOfcMYSaKXp$7CJ*2bM!7u|a#Q6v>vhg#Scb;DGQ zz?oPQ*Q2KLC|1DdSP3g6@#O?xMwO4F2Kd+-)X$V7QJcIv>Oq>Bde`Y{PYkiT=7f`p z+VyL#AEQ2qj-Y1b2UJJb(F-4=mgE`gHTFt24^j*@6Op!D)><*Up7pOnqTN{=)nF5Q z!ON(IyQ8MI7gof<)&;01{tRp29n_RY^*5WeC+hw;QJZcKw!od3fL;T5&FSB1PofbI z#G*LSmX}y}U=iw1Vln&$wP$<=nx)H+RVdd$olimy*hO783$<4kV0Ygpwv%7-bANvO7_RQvk>=EzJuBuTT&gT5q^bQ+u-56 z5m*Y#;ce8~hmA1j%cDQ#dLwxMHL@mD$PTDazC>JvqcIeNN17X>QA<=Cb$u(;lXpeU z+;r5YU5%R2gSP&h^?|JqPBYh)Ph$y=cqLfA7!RE3NUnGI{Xs#Ag53rUq$Y7orffP0`FJNOyomNZ7Ax77*xXz zQA^PS!*Mj`!?#fbUXE&KHx|Yt){CePA7Xj*A7ehkt7AC*JMBp{(!uD5BT-K@28-ZS zRL3jO8+W7Daz7TrZ&5RI(|RAHC_hE5edJj4-LVR4sVAWBn~MH=|Cf>!!L_K7AFvmk z!Ge_UpgQn=&3x$$vi8A6)PIa>ucfhiIqCsBBLi}sL=qn=`ePgp!H&2VTjLYdK$=Z4Upx}S=n35(7! z^`%j3Uk~*`)7IJtJ(SaId7dq=MSaeEf!TlmpHxD{DO5*iP*Zxx>NV5c5QgfgIBG_! zqMoRZwK-~Ox}Y}>Kz|%!>(fyWIt6`kF}g}tkm!cB7>FBDn{fx~{6WlKYt-iX9yQP_ zw)_OOn{&@HOH>$RD95Ar(g@TOr=uS9HPp;bn8o}LC3%+$t*IYB-oZ@N$jiKCW}*t} zZHUJj*b6n_cTfXcfO@husDbRV^+!=laux&e7Uso=sQY|ov;L|mINQ9(B~iP)F6O}z zs1d)8>R=M;^;?JuxYd@OIc75!#az_)L@h;c%!k8KGv;CoT!32QOR7+EAGPUx-!?Zy zVSdWhQEMHKdg4|XfC;DpcEx7c4=dp&tc+(c2L0xmZ%DCNjdC(}$Azf-+&@UFlf*Ia zdZJ+%jC-&Ep1?-<6RP8qndYzGirA2H4^%@-P)l~jdK>kD^#C=4!Sl>ggrV9gYjmAz zB%xH)Lv`E^+hHHn6K+Gzz&;GcBdCF$M}74Ej{1|#Z@%du8Z~pZQB&U>HGnRtP1qgF zV_zJn_y0|jB~%0~;9s(EEw;xB3(ejbhhdawqB>rI8dw&F;C@^F2G!w@*4r3K`6-sc zV*IYw{qd;!&Uk|UoeUC9-HY$qUn15-)TT*CH9QN|;bQA%)WG)H@@dp{*HJU}0JZD? zMz!z1*nE=;L4DB0psNNuk?4u~qc+({498KZ0n9;7{aV!9vfG~j67@uvQSbQ!RL74{ zYwWydraTafQw%}%7lUfQ(R<8)QIc1v(AuY?-hwHpCtHoWaVKhxzei2w18juFS+^qC z6E(2WsQad%I(iS)?iP&4eW(Y@wbZ-~;Y*o+ZI)J4)WUAq2_kSBnBo#MM z7v^4T8VErxO(^Py2-FjoM9olD)b({x4acLVI>D9`QA;=!OW<_$!*!@V@Bs$Vzq6l2 zYjqs+;w99`f71o{2z7(+I@56>)KZl|4X`A}Vg;;&iC7Ka#W*~My54WSnc)D8qFe#p zawG{PT7uD72D5NHob_F;d~MYA^-xRM zXe0Bl-QSjq7xPKJ8R3=Fqg{wc&LP94l$K{8S3dn$r}-$ zkmnq{g^mw(ZbyE!B+3#c2|axRMiK$+nii<{I_IcN-Ivr2Bm&7R;v7OxUz753e2Mbw z#7pGb;5yzWdJ^-9J%l}6r!Y6^D98<3vv@*lIE~mv-M7S6@(kiLLdQnx^4t5gVRMeR zsn0oHA$f~9O@5X=-_un_VFG+|4R%e+7PXXgG67h zt7aQ2NI8hmQH%N)a2&CW{7=-T`}c7!r;sx}DO~simHq6A8kC!m|3(}k&pB$7Xk)gr zb>EPCF$n!xtylPKLdRyxH;~`N&IsZ$p(7vl&+rS}M!3cKQHeN1{7oct@*R8~7ZN3j zKw==FPm!9OYe~8sb-aciVh{0`t*egp3Eoy`9)6yEC2tyeLt+r2-?kk5{ZFH7sF*@& zJP}BKjHsm3_V^iJq3#jkPyCk{MV%L>5kC-z2_3$)r(aZ~Dfh5-?eGG1`qD9-Jc9U} z{JGYD289(wRc<^=rH&Bt0P0Gkj?2Vg%H^_4>=)}utZeg}n1^d_5kqWUFY*RNW6Hyb z>*T%dx%s%krD8P6MdB{`OnXxJ+s=oR&*Yqr82s7h0hGHDYlsA*J@Jb@Hw(X`tV5q8 zI`Z1~lz*%r9PUzkgDLVAiKIPs#ke?~d>#1^T|*>6BvG3@7w0>XKOu6C>`ycbiG(-T zOd@LJoW!n_bwpzYGZ@$Tk-`ir?_e|HUGmoW2~mwaj~(?r@|+`-x?U7o*op%7nk|$Y z5ZAc(9lVOAh@9gOl2MeSxIP^}%*FcOvlpD@WLd(`Hl~|~ajq!k7ggXmXs<0revQ~d zxfwpiX#55{6Ymj)Dd!yBD0^&SHu)4Hl6E)Kzf;zBSeHs2Yl%(dLBtzGF!2#}MKFSE zb@^06M=WuP`Ve9}(UI~3o#2>jaMs}+BF>h@6RsUkxH~AE_}7WtRC-g+i#pn10b(5S zKIM{juxH3$)dd`riQUxkuo=PRCI4qr~szpAviRH9Du`5^yxei)a|$T{)m}GRnAQ&+%Xh> zC8=*O5XULE!;3foixE4>)9^op4iBA{pzeq%X8%9Jiz%0*{;I9JZWa4D_bHKclrn=h z>)+Sb91wJB9;23_S&J=6l*Q6%i#Pj%HI>wl&etg zM-(G;+?RU(i6p&kcrHh)OImV7;Nh+N0ZA~|r3%QP`L?80`wp^5QdGZCspsExWk}R<&i_q~(^0CzGsA+IkVs7P` z!v}|D{T|af&_6wWB(7*>rVCcO&ZuIWkA-h2A918l2RKdC8cJhq-QN|yelZH zUBbE0ik^PS{W6l$2c@QXQu}+-2M$W}BqybM`j`tn1N)?TvRg>*7nfBosh4+1lYg$a z^;r}8&(0Un_@7?$W{mU;7?6DE{gi&mBhxbvxS?6oUyJbxIJ96$>X6iw^wg{!?u`6d zadU!-R7)O|lI&^Rw{J$$z>Jguj3v$U&t++Jp6*E-lr+%uPit{mP1oP`D)@hQ{BtI& J%BBIi{s*k@Fwg)1 delta 12160 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 RJ0xq)mOtII7H#e0_CI69)-?bC diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 893d4384..b4b18615 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -26,7 +26,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-08-10 08:01+0200\n" +"POT-Creation-Date: 2022-08-10 08:37+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -241,6 +241,7 @@ msgstr "" #: ema/templates/ema/detail/includes/states-after.html:36 #: ema/templates/ema/detail/includes/states-before.html:36 #: intervention/forms/modalForms.py:364 +#: templates/email/other/deduction_changed.html:29 msgid "Surface" msgstr "Fläche" @@ -307,6 +308,7 @@ msgstr "Typ" #: intervention/forms/modalForms.py:382 intervention/tables.py:87 #: intervention/templates/intervention/detail/view.html:19 #: konova/templates/konova/includes/quickstart/interventions.html:4 +#: templates/email/other/deduction_changed.html:24 #: templates/navbars/navbar.html:22 msgid "Intervention" msgstr "Eingriff" @@ -696,14 +698,14 @@ msgstr "" msgid "Pieces" msgstr "Stück" -#: compensation/models/eco_account.py:55 +#: compensation/models/eco_account.py:56 msgid "" "Deductable surface can not be larger than existing surfaces in after states" msgstr "" "Die abbuchbare Fläche darf die Gesamtfläche der Zielzustände nicht " "überschreiten" -#: compensation/models/eco_account.py:62 +#: compensation/models/eco_account.py:63 msgid "" "Deductable surface can not be smaller than the sum of already existing " "deductions. Please contact the responsible users for the deductions!" @@ -1915,23 +1917,27 @@ msgstr "{} - Zugriff entzogen" msgid "{} - Shared access given" msgstr "{} - Zugriff freigegeben" -#: konova/utils/mailer.py:160 konova/utils/mailer.py:275 +#: konova/utils/mailer.py:160 konova/utils/mailer.py:302 msgid "{} - Shared data unrecorded" msgstr "{} - Freigegebene Daten entzeichnet" -#: konova/utils/mailer.py:183 konova/utils/mailer.py:252 +#: konova/utils/mailer.py:183 konova/utils/mailer.py:279 msgid "{} - Shared data recorded" msgstr "{} - Freigegebene Daten verzeichnet" -#: konova/utils/mailer.py:206 konova/utils/mailer.py:321 +#: konova/utils/mailer.py:206 konova/utils/mailer.py:348 msgid "{} - Shared data checked" msgstr "{} - Freigegebene Daten geprüft" -#: konova/utils/mailer.py:229 konova/utils/mailer.py:298 +#: konova/utils/mailer.py:233 konova/utils/mailer.py:372 +msgid "{} - Deduction changed" +msgstr "{} - Abbuchung geändert" + +#: konova/utils/mailer.py:256 konova/utils/mailer.py:325 msgid "{} - Shared data deleted" msgstr "{} - Freigegebene Daten gelöscht" -#: konova/utils/mailer.py:342 templates/email/api/verify_token.html:4 +#: konova/utils/mailer.py:393 templates/email/api/verify_token.html:4 msgid "Request for new API token" msgstr "Anfrage für neuen API Token" @@ -2235,6 +2241,7 @@ msgstr "" #: templates/email/checking/shared_data_checked_team.html:19 #: templates/email/deleting/shared_data_deleted.html:19 #: templates/email/deleting/shared_data_deleted_team.html:19 +#: templates/email/other/deduction_changed.html:38 #: templates/email/recording/shared_data_recorded.html:19 #: templates/email/recording/shared_data_recorded_team.html:19 #: templates/email/recording/shared_data_unrecorded.html:19 @@ -2253,6 +2260,7 @@ msgstr "Freigegebene Daten geprüft" #: templates/email/checking/shared_data_checked.html:8 #: templates/email/deleting/shared_data_deleted.html:8 +#: templates/email/other/deduction_changed.html:8 #: templates/email/recording/shared_data_recorded.html:8 #: templates/email/recording/shared_data_unrecorded.html:8 #: templates/email/sharing/shared_access_given.html:8 @@ -2295,6 +2303,7 @@ msgstr "der folgende Datensatz wurde soeben gelöscht " #: templates/email/deleting/shared_data_deleted.html:16 #: templates/email/deleting/shared_data_deleted_team.html:16 +#: templates/email/other/deduction_changed.html:35 msgid "" "If this should not have been happened, please contact us. See the signature " "for details." @@ -2302,6 +2311,31 @@ msgstr "" "Falls das nicht hätte passieren dürfen, kontaktieren Sie uns bitte. In der E-" "mail Signatur finden Sie weitere Kontaktinformationen." +#: templates/email/other/deduction_changed.html:4 +msgid "Deduction changed" +msgstr "Abbuchung geändert" + +#: templates/email/other/deduction_changed.html:10 +msgid "a deduction of this eco account has changed:" +msgstr "eine Abbuchung des Ökokontos hat sich geändert:" + +#: templates/email/other/deduction_changed.html:14 +msgid "Attribute" +msgstr "Attribute" + +#: templates/email/other/deduction_changed.html:15 +msgid "Old" +msgstr "Alt" + +#: templates/email/other/deduction_changed.html:16 +#: templates/generic_index.html:43 user/templates/user/team/index.html:22 +msgid "New" +msgstr "Neu" + +#: templates/email/other/deduction_changed.html:19 +msgid "EcoAccount" +msgstr "Ökokonto" + #: templates/email/recording/shared_data_recorded.html:4 #: templates/email/recording/shared_data_recorded_team.html:4 msgid "Shared data recorded" @@ -2499,10 +2533,6 @@ msgstr "* sind Pflichtfelder." msgid "New entry" msgstr "Neuer Eintrag" -#: templates/generic_index.html:43 user/templates/user/team/index.html:22 -msgid "New" -msgstr "Neu" - #: templates/generic_index.html:58 msgid "Search for keywords" msgstr "Nach Schlagwörtern suchen" diff --git a/templates/email/api/verify_token.html b/templates/email/api/verify_token.html index bd12a0fe..355647b2 100644 --- a/templates/email/api/verify_token.html +++ b/templates/email/api/verify_token.html @@ -6,6 +6,7 @@

{% trans 'Hello support' %},
+
{% trans 'you need to verify the API token for user' %}:

diff --git a/templates/email/checking/shared_data_checked.html b/templates/email/checking/shared_data_checked.html index 0b67ecc7..133dae2e 100644 --- a/templates/email/checking/shared_data_checked.html +++ b/templates/email/checking/shared_data_checked.html @@ -7,6 +7,7 @@
{% trans 'Hello ' %} {{user.username}},
+
{% trans 'the following dataset has just been checked' %}
{{obj_identifier}} diff --git a/templates/email/checking/shared_data_checked_team.html b/templates/email/checking/shared_data_checked_team.html index ee813811..6a7a4500 100644 --- a/templates/email/checking/shared_data_checked_team.html +++ b/templates/email/checking/shared_data_checked_team.html @@ -7,6 +7,7 @@
{% trans 'Hello team' %} {{team.name}},
+
{% trans 'the following dataset has just been checked' %}
{{obj_identifier}} diff --git a/templates/email/deleting/shared_data_deleted.html b/templates/email/deleting/shared_data_deleted.html index 272b0fde..7857444c 100644 --- a/templates/email/deleting/shared_data_deleted.html +++ b/templates/email/deleting/shared_data_deleted.html @@ -7,6 +7,7 @@
{% trans 'Hello ' %} {{user.username}},
+
{% trans 'the following dataset has just been deleted' %}
{{obj_identifier}} diff --git a/templates/email/deleting/shared_data_deleted_team.html b/templates/email/deleting/shared_data_deleted_team.html index cedb2a45..0ffa8bbf 100644 --- a/templates/email/deleting/shared_data_deleted_team.html +++ b/templates/email/deleting/shared_data_deleted_team.html @@ -7,6 +7,7 @@
{% trans 'Hello team' %} {{team.name}},
+
{% trans 'the following dataset has just been deleted' %}
{{obj_identifier}} diff --git a/templates/email/other/deduction_changed.html b/templates/email/other/deduction_changed.html new file mode 100644 index 00000000..8129f6b2 --- /dev/null +++ b/templates/email/other/deduction_changed.html @@ -0,0 +1,50 @@ +{% load i18n %} + +
+

{% translate 'Deduction changed' %}

+

{{obj_identifier}}

+
+
+ {% translate 'Hello ' %} {{user.username}}, +
+
+ {% translate 'a deduction of this eco account has changed:' %} +
+
+
+ + + + + + + + + + + + + + + + + + + + +
{% translate 'Attribute' %}{% translate 'Old' %}{% translate 'New' %}
{% translate 'EcoAccount' %}{{data_changes.account.old}}{{data_changes.account.new}}
{% translate 'Intervention' %}{{data_changes.intervention.old}}{{data_changes.intervention.new}}
{% translate 'Surface' %}{{data_changes.surface.old}} m²{{data_changes.surface.new}} m²
+
+
+ {% translate 'If this should not have been happened, please contact us. See the signature for details.' %} +
+
+ {% translate 'Best regards' %} +
+ KSP +
+
+
+ {% include 'email/signature.html' %} + +
+ diff --git a/templates/email/other/deduction_changed_team.html b/templates/email/other/deduction_changed_team.html new file mode 100644 index 00000000..babf36c0 --- /dev/null +++ b/templates/email/other/deduction_changed_team.html @@ -0,0 +1,50 @@ +{% load i18n %} + +
+

{% translate 'Deduction changed' %}

+

{{obj_identifier}}

+
+
+ {% trans 'Hello team' %} {{team.name}}, +
+
+ {% translate 'a deduction of this eco account has changed:' %} +
+
+ + + + + + + + + + + + + + + + + + + + + +
{% translate 'Attribute' %}{% translate 'Old' %}{% translate 'New' %}
{% translate 'EcoAccount' %}{{data_changes.account.old}}{{data_changes.account.new}}
{% translate 'Intervention' %}{{data_changes.intervention.old}}{{data_changes.intervention.new}}
{% translate 'Surface' %}{{data_changes.surface.old}} m²{{data_changes.surface.new}} m²
+
+
+ {% translate 'If this should not have been happened, please contact us. See the signature for details.' %} +
+
+ {% translate 'Best regards' %} +
+ KSP +
+
+
+ {% include 'email/signature.html' %} +
+
+ diff --git a/templates/email/recording/shared_data_recorded.html b/templates/email/recording/shared_data_recorded.html index 6805c928..ccad11e0 100644 --- a/templates/email/recording/shared_data_recorded.html +++ b/templates/email/recording/shared_data_recorded.html @@ -7,6 +7,7 @@
{% trans 'Hello ' %} {{user.username}},
+
{% trans 'the following dataset has just been recorded' %}
{{obj_identifier}} diff --git a/templates/email/recording/shared_data_recorded_team.html b/templates/email/recording/shared_data_recorded_team.html index 12efa8f6..0734bc62 100644 --- a/templates/email/recording/shared_data_recorded_team.html +++ b/templates/email/recording/shared_data_recorded_team.html @@ -7,6 +7,7 @@
{% trans 'Hello team' %} {{team.name}},
+
{% trans 'the following dataset has just been recorded' %}
{{obj_identifier}} diff --git a/templates/email/recording/shared_data_unrecorded.html b/templates/email/recording/shared_data_unrecorded.html index 1e0310ae..3406b750 100644 --- a/templates/email/recording/shared_data_unrecorded.html +++ b/templates/email/recording/shared_data_unrecorded.html @@ -7,6 +7,7 @@
{% trans 'Hello ' %} {{user.username}},
+
{% trans 'the following dataset has just been unrecorded' %}
{{obj_identifier}} diff --git a/templates/email/recording/shared_data_unrecorded_team.html b/templates/email/recording/shared_data_unrecorded_team.html index 64141555..5c0f8560 100644 --- a/templates/email/recording/shared_data_unrecorded_team.html +++ b/templates/email/recording/shared_data_unrecorded_team.html @@ -7,6 +7,7 @@
{% trans 'Hello team' %} {{team.name}},
+
{% trans 'the following dataset has just been unrecorded' %}
{{obj_identifier}} diff --git a/templates/email/sharing/shared_access_given.html b/templates/email/sharing/shared_access_given.html index 140e7a88..6ab759b3 100644 --- a/templates/email/sharing/shared_access_given.html +++ b/templates/email/sharing/shared_access_given.html @@ -7,6 +7,7 @@
{% trans 'Hello ' %} {{user.username}},
+
{% trans 'the following dataset has just been shared with you' %}
{{obj_identifier}} diff --git a/templates/email/sharing/shared_access_given_team.html b/templates/email/sharing/shared_access_given_team.html index 990ba2de..cdf3bb1c 100644 --- a/templates/email/sharing/shared_access_given_team.html +++ b/templates/email/sharing/shared_access_given_team.html @@ -7,6 +7,7 @@
{% trans 'Hello team' %} {{team.name}},
+
{% trans 'the following dataset has just been shared with your team' %}
{{obj_identifier}} diff --git a/templates/email/sharing/shared_access_removed.html b/templates/email/sharing/shared_access_removed.html index d1cbc5bf..b3121117 100644 --- a/templates/email/sharing/shared_access_removed.html +++ b/templates/email/sharing/shared_access_removed.html @@ -7,6 +7,7 @@
{% trans 'Hello ' %} {{user.username}},
+
{% trans 'your shared access, including editing, has been revoked for the dataset ' %}
{{obj_identifier}} diff --git a/templates/email/sharing/shared_access_removed_team.html b/templates/email/sharing/shared_access_removed_team.html index 5472ef73..992f00f8 100644 --- a/templates/email/sharing/shared_access_removed_team.html +++ b/templates/email/sharing/shared_access_removed_team.html @@ -7,6 +7,7 @@
{% trans 'Hello team' %} {{team.name}},
+
{% trans 'your teams shared access, including editing, has been revoked for the dataset ' %}
{{obj_identifier}} diff --git a/user/models/team.py b/user/models/team.py index 5e728e71..7162e977 100644 --- a/user/models/team.py +++ b/user/models/team.py @@ -95,6 +95,20 @@ class Team(UuidModel, DeletableObjectMixin): mailer = Mailer() mailer.send_mail_shared_data_checked_team(obj_identifier, obj_title, self) + def send_mail_deduction_changed(self, obj_identifier, obj_title, data_changes): + """ Sends a mail to the team members in case of changed deduction values + + Args: + obj_identifier (str): Identifier of the main object + obj_title (str): Title of the main object + data_changes (dict): Contains the old|new changes of the deduction changes + + Returns: + + """ + mailer = Mailer() + mailer.send_mail_deduction_changed_team(obj_identifier, obj_title, self, data_changes) + def send_mail_shared_data_deleted(self, obj_identifier, obj_title): """ Sends a mail to the team members in case of deleted data diff --git a/user/models/user.py b/user/models/user.py index b40a3b1b..20d3d50b 100644 --- a/user/models/user.py +++ b/user/models/user.py @@ -145,6 +145,22 @@ class User(AbstractUser): mailer = Mailer() mailer.send_mail_shared_data_checked(obj_identifier, obj_title, self) + def send_mail_deduction_changed(self, obj_identifier, obj_title, data_changes): + """ Sends a mail to the user in case of a changed deduction + + Args: + obj_identifier (str): Identifier of the main object + obj_title (str): Title of the main object + data_changes (dict): Contains the old|new changes of the deduction changes + + Returns: + + """ + notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_DEDUCTION_CHANGES) + if notification_set: + mailer = Mailer() + mailer.send_mail_deduction_changed(obj_identifier, obj_title, self, data_changes) + def get_API_token(self): """ Getter for an API token From 049305c32e1f7d7d8f1285debd2dfda7b88e6e01 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 10 Aug 2022 09:03:24 +0200 Subject: [PATCH 05/48] Minor changes * updates translation --- locale/de/LC_MESSAGES/django.mo | Bin 44532 -> 44517 bytes locale/de/LC_MESSAGES/django.po | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 6ee76a29ae6b97210a548c54b5f64f6f34f30f49..feda2678e509fe8e667237f2618882f674d2581e 100644 GIT binary patch delta 2071 zcmXZddu+{T9LMqRjykRNHpgW+t(L|~rFAKtxnyhEkhn!jSR!o6uqHzy86gfoL&R-K zt=-zHDq6PHrA30MZDEncR;IDc;xbt>MY2e`pk^`NAJ6mG>v_Jv-|zc{p!XMM>6nXYI2yHp$r#3II2dPR9M|G*e2N>f z@=LQ4j8>YZ;!^B|Yfy!Eq88SGs@EL&z)tFcKA?gqLtJs&j?a zWv)(BuObP&ZyfwX7Xq#RsSY|6&G)c9{*rEL6gYs5hF6 zUGPKfgCC(5upZU=N>oP<`|D?rjs$jSQ|>V=DXBm-;q?fmT+EdT=G`&32#?@4*S!gnFZYumHQ& zy0bD4M=_s@#kc{L?*~-p+AsyLqn^KuTG0QPLw(EK;{xL`%KQV&z_^cBdrzYlbPd(g zd#D1>P=_mA=L+>j<%ywQXaed`zvttnIE48o479T24CMEy#I2}=f1*0^5VgnQy)ID% zvzZS>C3*+7h09S}_=z`;`g%5@7O)kywbiJzR==12uVQeVzzEFS$0vmgkb`YEQJ>q2 z{d`JThq;(q?;M5Wn7@zNSc7?Z7S-x|sD(dAJ(t|Tr;LR-4dV@g*}Dub5?F$12i%*i zL7o0}s1b@taPJ|A+t?7!oKMnQ95mbi;pza?WFi_%=s8$#G zc?oI{=VO0djbW@poq;;kmNlcc>RZ&IyMkKyAO8A%)cuc9g%iJW3rt6yogl*C4F*vh zjU}kly$#3WS=1YKVm3a-9E^VLT3UqKf>MlO1FplLQF*3(A)cGqCLp)S@zf zp#qbbS7RdX$1pbf>&Lt&u`BTw)br<1_h0byOQ=q?p$_#;AMZfj_Xrd8{Xg>;LX9qA z7*#L@^5z+0M&((BipOyl^{s`$TueLc ze%Tg#1Jw6hh1qx%2jDN5gB{*pN8A}2gM)}qMEz}lj5?e(s0FuRGXCV}zu}2^PIPJc M)w0-$*0S8p7blZV+5i9m delta 2081 zcmXZce@xX?7{~D^zzZmtBv*~gMQ(% zzavSX?2H7Do7viCvsriqXXAM+!O(8AIarQmScj`|FKXRCI2SA4HLJ!3oP=NCJ$MG= zIEv{QYcVUunfM4k+hW0;KnEB|W8hEkRm`D3hAB8{k69LmQGp)tmSHyiN>t(+tik7T z0uEp*4q-NajoR13m9X90huT;$p!@lDhn+kRK_@pvaODq#sKK^${%CF<@s zVHCHaPS%N9cMNsKKcOl)hH=d0-tHy8Jx4=3YeX&FhDx*_75FGt;SlOXcfId^4bxC> zb3rjszh>!yW=n_Pz>|2 z3>9b@>I&aLUEx-50`==@L9J^;U2PBQtqpWg|5s_8U|=p*d}y`>8<2->S5QBZ~&*{U#NMhU2c6A>T0r4^P{K}$50h2Ma`cd&`{tiRH>_dzZP|e z^_Yj-F@)WyH_(r|vg4?$`VKSkchttO`R_MT^T&0XgLm51735Tj8}qZsF7EtcUv ztiaQ#1)&~S;&d#aUyMaqjkuw~b{Z)9CkL zDt?L~Jm$ZDCSjQD5=n*6Ze_V2XbK88j3i3l%VoN*F;c zyw881hbi Date: Wed, 10 Aug 2022 09:28:43 +0200 Subject: [PATCH 06/48] Fix * adds more detailed situation check on check_for_recorded_instance() --- konova/forms.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/konova/forms.py b/konova/forms.py index ced2bf10..67014260 100644 --- a/konova/forms.py +++ b/konova/forms.py @@ -15,6 +15,7 @@ from django.contrib import messages from django.contrib.gis import gdal from django.db.models.fields.files import FieldFile +from compensation.models import EcoAccount from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from user.models import User from django.contrib.gis.forms import MultiPolygonField @@ -156,16 +157,16 @@ class BaseForm(forms.Form): RemoveEcoAccountDeductionModalForm is_none = self.instance is None is_other_data_type = not isinstance(self.instance, BaseObject) - is_deduction_form = isinstance( + is_deduction_form_from_account = isinstance( self, ( NewDeductionModalForm, EditEcoAccountDeductionModalForm, RemoveEcoAccountDeductionModalForm, ) - ) + ) and isinstance(self.instance, EcoAccount) - if is_none or is_other_data_type or is_deduction_form: + if is_none or is_other_data_type or is_deduction_form_from_account: # Do nothing return From 7e05d05d97cb604bf0128b4b3a4814bd201287b9 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 15 Aug 2022 08:08:15 +0200 Subject: [PATCH 07/48] Model * adds new model and mixin * adds new functionality for Mailer class for sending resubmission mails --- compensation/models/compensation.py | 7 ++- intervention/models/intervention.py | 11 +++-- konova/models/__init__.py | 1 + konova/models/object.py | 22 ++++++++- konova/models/resubmission.py | 46 +++++++++++++++++++ konova/utils/mailer.py | 23 ++++++++++ .../email/resubmission/resubmission.html | 29 ++++++++++++ 7 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 konova/models/resubmission.py create mode 100644 templates/email/resubmission/resubmission.html diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index e513c95c..b65d259e 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -19,14 +19,17 @@ from compensation.managers import CompensationManager from compensation.models import CompensationState, CompensationAction from compensation.utils.quality import CompensationQualityChecker from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \ - GeoReferencedMixin, DeadlineType + GeoReferencedMixin, DeadlineType, ResubmitableObjectMixin from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \ DOCUMENT_REMOVED_TEMPLATE, DEADLINE_REMOVED, ADDED_DEADLINE, \ COMPENSATION_ACTION_REMOVED, COMPENSATION_STATE_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE from user.models import UserActionLogEntry -class AbstractCompensation(BaseObject, GeoReferencedMixin): +class AbstractCompensation(BaseObject, + GeoReferencedMixin, + ResubmitableObjectMixin + ): """ Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation, EMA or EcoAccount. diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index dd15beb1..ea561c5b 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -26,14 +26,19 @@ from intervention.models.revocation import RevocationDocument, Revocation from intervention.utils.quality import InterventionQualityChecker from konova.models import generate_document_file_upload_path, AbstractDocument, BaseObject, \ ShareableObjectMixin, \ - RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin -from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP + RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin, ResubmitableObjectMixin from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE, \ PAYMENT_REMOVED, PAYMENT_ADDED, REVOCATION_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE from user.models import UserActionLogEntry -class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin): +class Intervention(BaseObject, + ShareableObjectMixin, + RecordableObjectMixin, + CheckableObjectMixin, + GeoReferencedMixin, + ResubmitableObjectMixin + ): """ Interventions are e.g. construction sites where nature used to be. """ diff --git a/konova/models/__init__.py b/konova/models/__init__.py index c9156061..ba9de1d5 100644 --- a/konova/models/__init__.py +++ b/konova/models/__init__.py @@ -10,3 +10,4 @@ from .deadline import * from .document import * from .geometry import * from .parcel import * +from .resubmission import * diff --git a/konova/models/object.py b/konova/models/object.py index b468932a..0fbd6e8b 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -743,4 +743,24 @@ class GeoReferencedMixin(models.Model): zoom_lvl, x, y, - ) \ No newline at end of file + ) + + +class ResubmitableObjectMixin(models.Model): + resubmissions = models.ManyToManyField( + "konova.Resubmission", + null=True, + blank=True, + related_name="+", + ) + + class Meta: + abstract = True + + def resubmit(self): + """ Run resubmit check and run for all related resubmissions + + """ + resubmissions = self.resubmissions.all() + for resubmission in resubmissions: + resubmission.send_resubmission_mail(self.identifier) diff --git a/konova/models/resubmission.py b/konova/models/resubmission.py new file mode 100644 index 00000000..ca97ebbf --- /dev/null +++ b/konova/models/resubmission.py @@ -0,0 +1,46 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 15.08.22 + +""" +from dateutil.utils import today +from django.db import models + +from konova.models import BaseResource +from konova.utils.mailer import Mailer + + +class Resubmission(BaseResource): + user = models.ForeignKey( + "user.User", + on_delete=models.CASCADE, + help_text="The user who wants to be notifed" + ) + resubmit_on = models.DateField( + help_text="On which date the resubmission should be performed" + ) + resubmission_sent = models.BooleanField( + default=False, + help_text="Whether a resubmission has been sent or not" + ) + comment = models.TextField( + null=True, + blank=True, + help_text="Optional comment for the user itself" + ) + + def send_resubmission_mail(self, obj_identifier): + """ Sends a resubmission mail + + """ + _today = today() + resubmission_handled = _today.__ge__(self.resubmit_on) and self.resubmission_sent + if resubmission_handled: + return + + mailer = Mailer() + mailer.send_mail_resubmission(obj_identifier, self) + self.resubmission_sent = True + self.save() diff --git a/konova/utils/mailer.py b/konova/utils/mailer.py index 92bd2b60..8de91198 100644 --- a/konova/utils/mailer.py +++ b/konova/utils/mailer.py @@ -398,3 +398,26 @@ class Mailer: msg ) + def send_mail_resubmission(self, obj_identifier, resubmission): + """ Send a resubmission mail for a user + + Args: + obj_identifier (str): The (resubmitted) object's identifier + resubmission (Resubmission): The resubmission + + Returns: + + """ + context = { + "obj_identifier": obj_identifier, + "resubmission": resubmission, + "EMAIL_REPLY_TO": EMAIL_REPLY_TO, + } + msg = render_to_string("email/resubmission/resubmission.html", context) + user_mail_address = [SUPPORT_MAIL_RECIPIENT] + self.send( + user_mail_address, + _("Resubmission - {}").format(obj_identifier), + msg + ) + diff --git a/templates/email/resubmission/resubmission.html b/templates/email/resubmission/resubmission.html new file mode 100644 index 00000000..25848f55 --- /dev/null +++ b/templates/email/resubmission/resubmission.html @@ -0,0 +1,29 @@ +{% load i18n %} + +
+

{% trans 'Resubmission' %}

+

{{obj_identifier}}

+
+
+ {% trans 'Hello ' %} {{resubmission.user.username}}, +
+
+ {% trans 'you wanted to be reminded on this entry.' %} +
+ {% if resubmission.comment %} +
+ {% trans 'Your personal comment:' %} +
+
"{{resubmission.comment}}"
+ {% endif %} +
+
+ {% trans 'Best regards' %} +
+ KSP +
+
+ {% include 'email/signature.html' %} +
+
+ From 60867fdf392671bd07b3caa49610efadc6327e3b Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 15 Aug 2022 09:38:51 +0200 Subject: [PATCH 08/48] Templates + Routes * adds control button for Intervention, Compensation, Ema and EcoAccount for setting a resubmission on an entry --- .../compensation/includes/controls.html | 3 + .../detail/eco_account/includes/controls.html | 3 + compensation/urls/compensation.py | 1 + compensation/urls/eco_account.py | 1 + compensation/views/compensation.py | 26 ++++++- compensation/views/eco_account.py | 25 ++++++- .../ema/detail/includes/controls.html | 3 + ema/urls.py | 1 + ema/views.py | 27 ++++++- .../detail/includes/controls.html | 3 + intervention/urls.py | 4 +- intervention/views.py | 25 ++++++- konova/admin.py | 12 ++- konova/forms.py | 74 ++++++++++++++++++- 14 files changed, 200 insertions(+), 8 deletions(-) diff --git a/compensation/templates/compensation/detail/compensation/includes/controls.html b/compensation/templates/compensation/detail/compensation/includes/controls.html index 5be0b3e8..4119480e 100644 --- a/compensation/templates/compensation/detail/compensation/includes/controls.html +++ b/compensation/templates/compensation/detail/compensation/includes/controls.html @@ -12,6 +12,9 @@ {% if has_access %} + {% if is_default_member %} {% if has_access %} + diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index e1a41ff2..66020055 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -31,6 +31,7 @@ urlpatterns = [ path('/deadline//edit', deadline_edit_view, name='deadline-edit'), path('/deadline//remove', deadline_remove_view, name='deadline-remove'), path('/report', report_view, name='report'), + path('/resub', create_resubmission_view, name='resubmission-create'), # Documents path('/document/new/', new_document_view, name='new-doc'), diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index a3d1aa38..5a84e8ca 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -19,6 +19,7 @@ urlpatterns = [ path('/report', report_view, name='report'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), + path('/resub', create_resubmission_view, name='resubmission-create'), path('/state/new', state_new_view, name='new-state'), path('/state//edit', state_edit_view, name='state-edit'), diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py index efe51ce3..016f8ea9 100644 --- a/compensation/views/compensation.py +++ b/compensation/views/compensation.py @@ -14,7 +14,8 @@ from compensation.tables import CompensationTable from intervention.models import Intervention from konova.contexts import BaseContext from konova.decorators import * -from konova.forms import RemoveModalForm, SimpleGeomForm, RemoveDeadlineModalForm, EditDocumentModalForm +from konova.forms import RemoveModalForm, SimpleGeomForm, RemoveDeadlineModalForm, EditDocumentModalForm, \ + ResubmissionModalForm from konova.models import Deadline from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.documents import get_document, remove_document @@ -656,3 +657,26 @@ def report_view(request: HttpRequest, id: str): } context = BaseContext(request, context).context return render(request, template, context) + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def create_resubmission_view(request: HttpRequest, id: str): + """ Renders resubmission form for a compensation + + Args: + request (HttpRequest): The incoming request + id (str): Compensation's id + + Returns: + + """ + com = get_object_or_404(Compensation, id=id) + form = ResubmissionModalForm(request.POST or None, instance=com, request=request) + form.action_url = reverse("compensation:resubmission-create", args=(id,)) + return form.process_request( + request, + msg_success=_("Resubmission set"), + redirect_url=reverse("compensation:detail", args=(id,)) + ) diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index ebface8d..03109a8c 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -26,7 +26,7 @@ from konova.contexts import BaseContext from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \ shared_access_required from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentModalForm, RecordModalForm, \ - RemoveDeadlineModalForm, EditDocumentModalForm + RemoveDeadlineModalForm, EditDocumentModalForm, ResubmissionModalForm from konova.models import Deadline from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER @@ -838,4 +838,27 @@ def create_share_view(request: HttpRequest, id: str): return form.process_request( request, msg_success=_("Share settings updated") + ) + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def create_resubmission_view(request: HttpRequest, id: str): + """ Renders resubmission form for an eco account + + Args: + request (HttpRequest): The incoming request + id (str): EcoAccount's id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + form = ResubmissionModalForm(request.POST or None, instance=acc, request=request) + form.action_url = reverse("compensation:acc:resubmission-create", args=(id,)) + return form.process_request( + request, + msg_success=_("Resubmission set"), + redirect_url=reverse("compensation:acc:detail", args=(id,)) ) \ No newline at end of file diff --git a/ema/templates/ema/detail/includes/controls.html b/ema/templates/ema/detail/includes/controls.html index 6a4f7062..a16071bf 100644 --- a/ema/templates/ema/detail/includes/controls.html +++ b/ema/templates/ema/detail/includes/controls.html @@ -12,6 +12,9 @@ {% if has_access %} + diff --git a/ema/urls.py b/ema/urls.py index 90cafb66..63073d6e 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -19,6 +19,7 @@ urlpatterns = [ path('/remove', remove_view, name='remove'), path('/record', record_view, name='record'), path('/report', report_view, name='report'), + path('/resub', create_resubmission_view, name='resubmission-create'), path('/state/new', state_new_view, name='new-state'), path('/state//remove', state_remove_view, name='state-remove'), diff --git a/ema/views.py b/ema/views.py index 589165f5..9cd6dd9d 100644 --- a/ema/views.py +++ b/ema/views.py @@ -17,7 +17,7 @@ from konova.contexts import BaseContext from konova.decorators import conservation_office_group_required, shared_access_required from ema.models import Ema, EmaDocument from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm, RemoveDeadlineModalForm, \ - EditDocumentModalForm + EditDocumentModalForm, ResubmissionModalForm from konova.models import Deadline from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER @@ -710,4 +710,27 @@ def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): request, msg_success=DEADLINE_REMOVED, redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) \ No newline at end of file + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def create_resubmission_view(request: HttpRequest, id: str): + """ Renders resubmission form for an EMA + + Args: + request (HttpRequest): The incoming request + id (str): EMA's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + form = ResubmissionModalForm(request.POST or None, instance=ema, request=request) + form.action_url = reverse("ema:resubmission-create", args=(id,)) + return form.process_request( + request, + msg_success=_("Resubmission set"), + redirect_url=reverse("ema:detail", args=(id,)) + ) diff --git a/intervention/templates/intervention/detail/includes/controls.html b/intervention/templates/intervention/detail/includes/controls.html index f41c8b85..7af2165b 100644 --- a/intervention/templates/intervention/detail/includes/controls.html +++ b/intervention/templates/intervention/detail/includes/controls.html @@ -12,6 +12,9 @@ {% if has_access %} + diff --git a/intervention/urls.py b/intervention/urls.py index 2a5e6d38..c7c43837 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -10,7 +10,8 @@ from django.urls import path from intervention.views import index_view, new_view, detail_view, edit_view, remove_view, new_document_view, share_view, \ create_share_view, remove_revocation_view, new_revocation_view, check_view, log_view, new_deduction_view, \ record_view, remove_document_view, get_document_view, get_revocation_view, new_id_view, report_view, \ - remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view + remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view, \ + create_resubmission_view app_name = "intervention" urlpatterns = [ @@ -26,6 +27,7 @@ urlpatterns = [ path('/check', check_view, name='check'), path('/record', record_view, name='record'), path('/report', report_view, name='report'), + path('/resub', create_resubmission_view, name='resubmission-create'), # Compensations path('/compensation//remove', remove_compensation_view, name='remove-compensation'), diff --git a/intervention/views.py b/intervention/views.py index 36577202..c55fe722 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -12,7 +12,7 @@ from intervention.models import Intervention, Revocation, InterventionDocument, from intervention.tables import InterventionTable from konova.contexts import BaseContext from konova.decorators import * -from konova.forms import SimpleGeomForm, RemoveModalForm, RecordModalForm, EditDocumentModalForm +from konova.forms import SimpleGeomForm, RemoveModalForm, RecordModalForm, EditDocumentModalForm, ResubmissionModalForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.documents import remove_document, get_document from konova.utils.generators import generate_qr_code @@ -475,6 +475,29 @@ def create_share_view(request: HttpRequest, id: str): ) +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def create_resubmission_view(request: HttpRequest, id: str): + """ Renders resubmission form for an intervention + + Args: + request (HttpRequest): The incoming request + id (str): Intervention's id + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + form = ResubmissionModalForm(request.POST or None, instance=intervention, request=request) + form.action_url = reverse("intervention:resubmission-create", args=(id,)) + return form.process_request( + request, + msg_success=_("Resubmission set"), + redirect_url=reverse("intervention:detail", args=(id,)) + ) + + @login_required @registration_office_group_required @shared_access_required(Intervention, "id") diff --git a/konova/admin.py b/konova/admin.py index b30f4b14..b40a44c8 100644 --- a/konova/admin.py +++ b/konova/admin.py @@ -7,7 +7,7 @@ Created on: 22.07.21 """ from django.contrib import admin -from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup +from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup, Resubmission from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE from user.models import UserAction @@ -139,6 +139,15 @@ class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin): ] +class ResubmissionAdmin(BaseResourceAdmin): + list_display = [ + "resubmit_on" + ] + fields = [ + "comment", + "resubmit_on" + ] + # Outcommented for a cleaner admin backend on production #admin.site.register(Geometry, GeometryAdmin) @@ -148,3 +157,4 @@ class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin): #admin.site.register(ParcelGroup, ParcelGroupAdmin) #admin.site.register(GeometryConflict, GeometryConflictAdmin) #admin.site.register(Deadline, DeadlineAdmin) +#admin.site.register(Resubmission, ResubmissionAdmin) diff --git a/konova/forms.py b/konova/forms.py index 67014260..5d8f38e2 100644 --- a/konova/forms.py +++ b/konova/forms.py @@ -13,7 +13,9 @@ from bootstrap_modal_forms.utils import is_ajax from django import forms from django.contrib import messages from django.contrib.gis import gdal +from django.core.exceptions import ObjectDoesNotExist from django.db.models.fields.files import FieldFile +from django.utils.timezone import now from compensation.models import EcoAccount from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP @@ -26,7 +28,7 @@ from django.shortcuts import render from django.utils.translation import gettext_lazy as _ from konova.contexts import BaseContext -from konova.models import BaseObject, Geometry, RecordableObjectMixin, AbstractDocument +from konova.models import BaseObject, Geometry, RecordableObjectMixin, AbstractDocument, Resubmission from konova.settings import DEFAULT_SRID from konova.tasks import celery_update_parcels from konova.utils.message_templates import FORM_INVALID, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE, DOCUMENT_EDITED @@ -161,6 +163,7 @@ class BaseForm(forms.Form): self, ( NewDeductionModalForm, + ResubmissionModalForm, EditEcoAccountDeductionModalForm, RemoveEcoAccountDeductionModalForm, ) @@ -686,3 +689,72 @@ class RecordModalForm(BaseModalForm): """ pass + + +class ResubmissionModalForm(BaseModalForm): + date = forms.DateField( + label_suffix=_(""), + label=_("Date"), + help_text=_("When do you want to be reminded?"), + widget=forms.DateInput( + attrs={ + "type": "date", + "data-provide": "datepicker", + "class": "form-control", + }, + format="%d.%m.%Y" + ) + ) + comment = forms.CharField( + required=False, + label=_("Comment"), + label_suffix=_(""), + help_text=_("Additional comment"), + widget=forms.Textarea( + attrs={ + "cols": 30, + "rows": 5, + "class": "form-control", + } + ) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Resubmission") + self.form_caption = _("Set your resubmission for this entry.") + self.action_url = None + + try: + self.resubmission = self.instance.resubmissions.get( + user=self.user + ) + self.initialize_form_field("date", str(self.resubmission.resubmit_on)) + self.initialize_form_field("comment", self.resubmission.comment) + except ObjectDoesNotExist: + self.resubmission = Resubmission() + + def is_valid(self): + super_valid = super().is_valid() + self_valid = True + + date = self.cleaned_data.get("date") + today = now().today().date() + if date <= today: + self.add_error( + "date", + _("The date should be in the future") + ) + self_valid = False + + return super_valid and self_valid + + def save(self): + with transaction.atomic(): + self.resubmission.user = self.user + self.resubmission.resubmit_on = self.cleaned_data.get("date") + self.resubmission.comment = self.cleaned_data.get("comment") + self.resubmission.save() + self.instance.resubmissions.add(self.resubmission) + return self.resubmission + From 8a446818032ac489eac6acf5bf0f19653502560a Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 15 Aug 2022 10:02:07 +0200 Subject: [PATCH 09/48] Command * adds new command to be used with cron for periodic checkin of resubmissions * updates translations --- konova/admin.py | 3 +- .../commands/handle_resubmissions.py | 46 +++ konova/models/resubmission.py | 2 +- locale/de/LC_MESSAGES/django.mo | Bin 44517 -> 45149 bytes locale/de/LC_MESSAGES/django.po | 302 ++++++++++-------- 5 files changed, 226 insertions(+), 127 deletions(-) create mode 100644 konova/management/commands/handle_resubmissions.py diff --git a/konova/admin.py b/konova/admin.py index b40a44c8..07d692d7 100644 --- a/konova/admin.py +++ b/konova/admin.py @@ -145,7 +145,8 @@ class ResubmissionAdmin(BaseResourceAdmin): ] fields = [ "comment", - "resubmit_on" + "resubmit_on", + "resubmission_sent", ] diff --git a/konova/management/commands/handle_resubmissions.py b/konova/management/commands/handle_resubmissions.py new file mode 100644 index 00000000..047dbd5c --- /dev/null +++ b/konova/management/commands/handle_resubmissions.py @@ -0,0 +1,46 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 15.08.22 + +""" +import datetime + +from compensation.models import Compensation, EcoAccount +from ema.models import Ema +from intervention.models import Intervention +from konova.management.commands.setup import BaseKonovaCommand +from konova.models import Resubmission + + +class Command(BaseKonovaCommand): + help = "Checks for resubmissions due now" + + def handle(self, *args, **options): + try: + resubmitable_models = [ + Intervention, + Compensation, + Ema, + EcoAccount, + ] + today = datetime.date.today() + resubmissions = Resubmission.objects.filter( + resubmit_on__lte=today, + resubmission_sent=False, + ) + self._write_warning(f"Found {resubmissions.count()} resubmission. Process now...") + for model in resubmitable_models: + all_objs = model.objects.filter( + resubmissions__in=resubmissions + ) + self._write_warning(f"Process resubmissions for {all_objs.count()} {model.__name__} entries") + for obj in all_objs: + obj.resubmit() + self._write_success("Mails have been sent.") + resubmissions.delete() + self._write_success("Resubmissions have been deleted.") + except KeyboardInterrupt: + self._break_line() + exit(-1) \ No newline at end of file diff --git a/konova/models/resubmission.py b/konova/models/resubmission.py index ca97ebbf..be5fe842 100644 --- a/konova/models/resubmission.py +++ b/konova/models/resubmission.py @@ -35,7 +35,7 @@ class Resubmission(BaseResource): """ Sends a resubmission mail """ - _today = today() + _today = today().date() resubmission_handled = _today.__ge__(self.resubmit_on) and self.resubmission_sent if resubmission_handled: return diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index feda2678e509fe8e667237f2618882f674d2581e..03853f485e18ce219bb50fbd9b452d147282bbb3 100644 GIT binary patch delta 12882 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\n" "Language-Team: LANGUAGE \n" @@ -85,7 +86,7 @@ msgstr "Bericht generieren" msgid "Select a timespan and the desired conservation office" msgstr "Wählen Sie die Zeitspanne und die gewünschte Eintragungsstelle" -#: analysis/forms.py:71 konova/forms.py:227 +#: analysis/forms.py:71 konova/forms.py:231 msgid "Continue" msgstr "Weiter" @@ -241,7 +242,8 @@ msgstr "" #: ema/templates/ema/detail/includes/states-after.html:36 #: ema/templates/ema/detail/includes/states-before.html:36 #: intervention/forms/modalForms.py:364 -#: templates/email/other/deduction_changed.html:29 +#: templates/email/other/deduction_changed.html:31 +#: templates/email/other/deduction_changed_team.html:31 msgid "Surface" msgstr "Fläche" @@ -308,7 +310,8 @@ msgstr "Typ" #: intervention/forms/modalForms.py:382 intervention/tables.py:87 #: intervention/templates/intervention/detail/view.html:19 #: konova/templates/konova/includes/quickstart/interventions.html:4 -#: templates/email/other/deduction_changed.html:24 +#: templates/email/other/deduction_changed.html:26 +#: templates/email/other/deduction_changed_team.html:26 #: templates/navbars/navbar.html:22 msgid "Intervention" msgstr "Eingriff" @@ -362,7 +365,7 @@ msgstr "Automatisch generiert" #: intervention/templates/intervention/detail/includes/documents.html:28 #: intervention/templates/intervention/detail/view.html:31 #: intervention/templates/intervention/report/report.html:12 -#: konova/forms.py:438 +#: konova/forms.py:442 msgid "Title" msgstr "Bezeichnung" @@ -389,12 +392,13 @@ msgstr "Kompensation XY; Flur ABC" #: intervention/templates/intervention/detail/includes/documents.html:34 #: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/revocation.html:38 -#: konova/forms.py:473 konova/templates/konova/includes/comment_card.html:16 +#: konova/forms.py:477 konova/forms.py:710 +#: konova/templates/konova/includes/comment_card.html:16 msgid "Comment" msgstr "Kommentar" #: compensation/forms/forms.py:59 compensation/forms/modalForms.py:471 -#: intervention/forms/forms.py:200 +#: intervention/forms/forms.py:200 konova/forms.py:712 msgid "Additional comment" msgstr "Zusätzlicher Kommentar" @@ -479,7 +483,7 @@ msgstr "kompensiert Eingriff" msgid "Select the intervention for which this compensation compensates" msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist" -#: compensation/forms/forms.py:219 compensation/views/compensation.py:110 +#: compensation/forms/forms.py:219 compensation/views/compensation.py:111 msgid "New compensation" msgstr "Neue Kompensation" @@ -531,7 +535,7 @@ msgid "Due on which date" msgstr "Zahlung wird an diesem Datum erwartet" #: compensation/forms/modalForms.py:65 compensation/forms/modalForms.py:363 -#: intervention/forms/modalForms.py:177 konova/forms.py:475 +#: intervention/forms/modalForms.py:177 konova/forms.py:479 msgid "Additional comment, maximum {} letters" msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" @@ -576,7 +580,7 @@ msgstr "Neuer Zustand" msgid "Insert data for the new state" msgstr "Geben Sie die Daten des neuen Zustandes ein" -#: compensation/forms/modalForms.py:219 konova/forms.py:229 +#: compensation/forms/modalForms.py:219 konova/forms.py:233 msgid "Object removed" msgstr "Objekt entfernt" @@ -602,7 +606,7 @@ msgstr "Fristart wählen" #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:36 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:36 #: ema/templates/ema/detail/includes/deadlines.html:36 -#: intervention/forms/modalForms.py:149 +#: intervention/forms/modalForms.py:149 konova/forms.py:697 msgid "Date" msgstr "Datum" @@ -850,24 +854,32 @@ msgstr "In LANIS öffnen" msgid "Public report" msgstr "Öffentlicher Bericht" -#: compensation/templates/compensation/detail/compensation/includes/controls.html:17 -#: compensation/templates/compensation/detail/eco_account/includes/controls.html:31 -#: ema/templates/ema/detail/includes/controls.html:31 -#: intervention/templates/intervention/detail/includes/controls.html:36 +#: compensation/templates/compensation/detail/compensation/includes/controls.html:15 +#: compensation/templates/compensation/detail/eco_account/includes/controls.html:15 +#: ema/templates/ema/detail/includes/controls.html:15 +#: intervention/templates/intervention/detail/includes/controls.html:15 +#: konova/forms.py:724 templates/email/resubmission/resubmission.html:4 +msgid "Resubmission" +msgstr "Wiedervorlage" + +#: compensation/templates/compensation/detail/compensation/includes/controls.html:20 +#: compensation/templates/compensation/detail/eco_account/includes/controls.html:34 +#: ema/templates/ema/detail/includes/controls.html:34 +#: intervention/templates/intervention/detail/includes/controls.html:39 msgid "Edit" msgstr "Bearbeiten" -#: compensation/templates/compensation/detail/compensation/includes/controls.html:21 -#: compensation/templates/compensation/detail/eco_account/includes/controls.html:35 -#: ema/templates/ema/detail/includes/controls.html:35 -#: intervention/templates/intervention/detail/includes/controls.html:40 -msgid "Show log" -msgstr "Log anzeigen" - #: compensation/templates/compensation/detail/compensation/includes/controls.html:24 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:38 #: ema/templates/ema/detail/includes/controls.html:38 #: intervention/templates/intervention/detail/includes/controls.html:43 +msgid "Show log" +msgstr "Log anzeigen" + +#: compensation/templates/compensation/detail/compensation/includes/controls.html:27 +#: compensation/templates/compensation/detail/eco_account/includes/controls.html:41 +#: ema/templates/ema/detail/includes/controls.html:41 +#: intervention/templates/intervention/detail/includes/controls.html:46 #: venv/lib/python3.7/site-packages/django/forms/formsets.py:391 msgid "Delete" msgstr "Löschen" @@ -907,7 +919,7 @@ msgstr "Dokumente" #: compensation/templates/compensation/detail/eco_account/includes/documents.html:14 #: ema/templates/ema/detail/includes/documents.html:14 #: intervention/templates/intervention/detail/includes/documents.html:14 -#: konova/forms.py:491 +#: konova/forms.py:495 msgid "Add new document" msgstr "Neues Dokument hinzufügen" @@ -915,7 +927,7 @@ msgstr "Neues Dokument hinzufügen" #: compensation/templates/compensation/detail/eco_account/includes/documents.html:31 #: ema/templates/ema/detail/includes/documents.html:31 #: intervention/templates/intervention/detail/includes/documents.html:31 -#: konova/forms.py:448 +#: konova/forms.py:452 msgid "Created on" msgstr "Erstellt" @@ -923,7 +935,7 @@ msgstr "Erstellt" #: compensation/templates/compensation/detail/eco_account/includes/documents.html:61 #: ema/templates/ema/detail/includes/documents.html:61 #: intervention/templates/intervention/detail/includes/documents.html:65 -#: konova/forms.py:553 +#: konova/forms.py:557 msgid "Edit document" msgstr "Dokument bearbeiten" @@ -1093,22 +1105,22 @@ msgstr "" msgid "other users" msgstr "weitere Nutzer" -#: compensation/templates/compensation/detail/eco_account/includes/controls.html:15 -#: ema/templates/ema/detail/includes/controls.html:15 +#: compensation/templates/compensation/detail/eco_account/includes/controls.html:18 +#: ema/templates/ema/detail/includes/controls.html:18 #: intervention/forms/modalForms.py:71 -#: intervention/templates/intervention/detail/includes/controls.html:15 +#: intervention/templates/intervention/detail/includes/controls.html:18 msgid "Share" msgstr "Freigabe" -#: compensation/templates/compensation/detail/eco_account/includes/controls.html:20 -#: ema/templates/ema/detail/includes/controls.html:20 -#: intervention/templates/intervention/detail/includes/controls.html:25 +#: compensation/templates/compensation/detail/eco_account/includes/controls.html:23 +#: ema/templates/ema/detail/includes/controls.html:23 +#: intervention/templates/intervention/detail/includes/controls.html:28 msgid "Unrecord" msgstr "Entzeichnen" -#: compensation/templates/compensation/detail/eco_account/includes/controls.html:24 -#: ema/templates/ema/detail/includes/controls.html:24 -#: intervention/templates/intervention/detail/includes/controls.html:29 +#: compensation/templates/compensation/detail/eco_account/includes/controls.html:27 +#: ema/templates/ema/detail/includes/controls.html:27 +#: intervention/templates/intervention/detail/includes/controls.html:32 msgid "Record" msgstr "Verzeichnen" @@ -1215,29 +1227,34 @@ msgstr "" msgid "Responsible data" msgstr "Daten zu den verantwortlichen Stellen" -#: compensation/views/compensation.py:53 +#: compensation/views/compensation.py:54 msgid "Compensations - Overview" msgstr "Kompensationen - Übersicht" -#: compensation/views/compensation.py:172 konova/utils/message_templates.py:36 +#: compensation/views/compensation.py:173 konova/utils/message_templates.py:36 msgid "Compensation {} edited" msgstr "Kompensation {} bearbeitet" -#: compensation/views/compensation.py:182 compensation/views/eco_account.py:173 +#: compensation/views/compensation.py:183 compensation/views/eco_account.py:173 #: ema/views.py:241 intervention/views.py:338 msgid "Edit {}" msgstr "Bearbeite {}" -#: compensation/views/compensation.py:269 compensation/views/eco_account.py:360 -#: ema/views.py:195 intervention/views.py:542 +#: compensation/views/compensation.py:270 compensation/views/eco_account.py:360 +#: ema/views.py:195 intervention/views.py:565 msgid "Log" msgstr "Log" -#: compensation/views/compensation.py:613 compensation/views/eco_account.py:728 -#: ema/views.py:559 intervention/views.py:688 +#: compensation/views/compensation.py:614 compensation/views/eco_account.py:728 +#: ema/views.py:559 intervention/views.py:711 msgid "Report {}" msgstr "Bericht {}" +#: compensation/views/compensation.py:680 compensation/views/eco_account.py:862 +#: ema/views.py:734 intervention/views.py:496 +msgid "Resubmission set" +msgstr "Wiedervorlage gesetzt" + #: compensation/views/eco_account.py:65 msgid "Eco-account - Overview" msgstr "Ökokonten - Übersicht" @@ -1255,12 +1272,12 @@ msgid "Eco-account removed" msgstr "Ökokonto entfernt" #: compensation/views/eco_account.py:381 ema/views.py:283 -#: intervention/views.py:641 +#: intervention/views.py:664 msgid "{} unrecorded" msgstr "{} entzeichnet" #: compensation/views/eco_account.py:381 ema/views.py:283 -#: intervention/views.py:641 +#: intervention/views.py:664 msgid "{} recorded" msgstr "{} verzeichnet" @@ -1462,11 +1479,11 @@ msgid "Checked compensations data and payments" msgstr "Kompensationen und Zahlungen geprüft" #: intervention/forms/modalForms.py:263 -#: intervention/templates/intervention/detail/includes/controls.html:19 +#: intervention/templates/intervention/detail/includes/controls.html:22 msgid "Run check" msgstr "Prüfung vornehmen" -#: intervention/forms/modalForms.py:264 konova/forms.py:594 +#: intervention/forms/modalForms.py:264 konova/forms.py:598 msgid "" "I, {} {}, confirm that all necessary control steps have been performed by " "myself." @@ -1622,11 +1639,11 @@ msgstr "Eingriff {} bearbeitet" msgid "{} removed" msgstr "{} entfernt" -#: intervention/views.py:495 +#: intervention/views.py:518 msgid "Check performed" msgstr "Prüfung durchgeführt" -#: intervention/views.py:646 +#: intervention/views.py:669 msgid "There are errors on this intervention:" msgstr "Es liegen Fehler in diesem Eingriff vor:" @@ -1711,78 +1728,90 @@ msgstr "Nach Zulassungsbehörde suchen" msgid "Search for conservation office" msgstr "Nch Eintragungsstelle suchen" -#: konova/forms.py:41 templates/form/collapsable/form.html:62 +#: konova/forms.py:44 templates/form/collapsable/form.html:62 msgid "Save" msgstr "Speichern" -#: konova/forms.py:75 +#: konova/forms.py:78 msgid "Not editable" msgstr "Nicht editierbar" -#: konova/forms.py:178 konova/forms.py:394 +#: konova/forms.py:182 konova/forms.py:398 msgid "Confirm" msgstr "Bestätige" -#: konova/forms.py:190 konova/forms.py:403 +#: konova/forms.py:194 konova/forms.py:407 msgid "Remove" msgstr "Löschen" -#: konova/forms.py:192 +#: konova/forms.py:196 msgid "You are about to remove {} {}" msgstr "Sie sind dabei {} {} zu löschen" -#: konova/forms.py:280 konova/utils/quality.py:44 konova/utils/quality.py:46 +#: konova/forms.py:284 konova/utils/quality.py:44 konova/utils/quality.py:46 #: templates/form/collapsable/form.html:45 msgid "Geometry" msgstr "Geometrie" -#: konova/forms.py:331 +#: konova/forms.py:335 msgid "Only surfaces allowed. Points or lines must be buffered." msgstr "" "Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden." -#: konova/forms.py:404 +#: konova/forms.py:408 msgid "Are you sure?" msgstr "Sind Sie sicher?" -#: konova/forms.py:450 +#: konova/forms.py:454 msgid "When has this file been created? Important for photos." msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?" -#: konova/forms.py:461 +#: konova/forms.py:465 #: venv/lib/python3.7/site-packages/django/db/models/fields/files.py:231 msgid "File" msgstr "Datei" -#: konova/forms.py:463 +#: konova/forms.py:467 msgid "Allowed formats: pdf, jpg, png. Max size 15 MB." msgstr "Formate: pdf, jpg, png. Maximal 15 MB." -#: konova/forms.py:528 +#: konova/forms.py:532 msgid "Added document" msgstr "Dokument hinzugefügt" -#: konova/forms.py:585 +#: konova/forms.py:589 msgid "Confirm record" msgstr "Verzeichnen bestätigen" -#: konova/forms.py:593 +#: konova/forms.py:597 msgid "Record data" msgstr "Daten verzeichnen" -#: konova/forms.py:600 +#: konova/forms.py:604 msgid "Confirm unrecord" msgstr "Entzeichnen bestätigen" -#: konova/forms.py:601 +#: konova/forms.py:605 msgid "Unrecord data" msgstr "Daten entzeichnen" -#: konova/forms.py:602 +#: konova/forms.py:606 msgid "I, {} {}, confirm that this data must be unrecorded." msgstr "" "Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen." +#: konova/forms.py:698 +msgid "When do you want to be reminded?" +msgstr "Wann wollen Sie erinnert werden?" + +#: konova/forms.py:725 +msgid "Set your resubmission for this entry." +msgstr "Setzen Sie eine Wiedervorlage für diesen Eintrag." + +#: konova/forms.py:746 +msgid "The date should be in the future" +msgstr "Das Datum sollte in der Zukunft liegen" + #: konova/management/commands/setup_data.py:26 msgid "On shared access gained" msgstr "Wenn mir eine Freigabe zu Daten erteilt wird" @@ -1929,7 +1958,7 @@ msgstr "{} - Freigegebene Daten verzeichnet" msgid "{} - Shared data checked" msgstr "{} - Freigegebene Daten geprüft" -#: konova/utils/mailer.py:233 konova/utils/mailer.py:372 +#: konova/utils/mailer.py:233 konova/utils/mailer.py:376 msgid "{} - Deduction changed" msgstr "{} - Abbuchung geändert" @@ -1937,10 +1966,14 @@ msgstr "{} - Abbuchung geändert" msgid "{} - Shared data deleted" msgstr "{} - Freigegebene Daten gelöscht" -#: konova/utils/mailer.py:393 templates/email/api/verify_token.html:4 +#: konova/utils/mailer.py:397 templates/email/api/verify_token.html:4 msgid "Request for new API token" msgstr "Anfrage für neuen API Token" +#: konova/utils/mailer.py:420 +msgid "Resubmission - {}" +msgstr "Wiedervorlage - {}" + #: konova/utils/message_templates.py:10 msgid "no further details" msgstr "keine weitere Angabe" @@ -2223,11 +2256,11 @@ msgstr "Irgendetwas ist passiert. Wir arbeiten daran!" msgid "Hello support" msgstr "Hallo Support" -#: templates/email/api/verify_token.html:9 +#: templates/email/api/verify_token.html:10 msgid "you need to verify the API token for user" msgstr "Sie müssen einen API Token für folgenden Nutzer freischalten" -#: templates/email/api/verify_token.html:15 +#: templates/email/api/verify_token.html:16 msgid "" "If unsure, please contact the user. The API token can not be used until you " "activated it in the admin backend." @@ -2236,20 +2269,22 @@ msgstr "" "Token kann so lange nicht verwendet werden, wie er noch nicht von Ihnen im " "Admin Backend aktiviert worden ist." -#: templates/email/api/verify_token.html:18 -#: templates/email/checking/shared_data_checked.html:19 -#: templates/email/checking/shared_data_checked_team.html:19 -#: templates/email/deleting/shared_data_deleted.html:19 -#: templates/email/deleting/shared_data_deleted_team.html:19 -#: templates/email/other/deduction_changed.html:38 -#: templates/email/recording/shared_data_recorded.html:19 -#: templates/email/recording/shared_data_recorded_team.html:19 -#: templates/email/recording/shared_data_unrecorded.html:19 -#: templates/email/recording/shared_data_unrecorded_team.html:19 -#: templates/email/sharing/shared_access_given.html:20 -#: templates/email/sharing/shared_access_given_team.html:20 -#: templates/email/sharing/shared_access_removed.html:20 -#: templates/email/sharing/shared_access_removed_team.html:20 +#: templates/email/api/verify_token.html:19 +#: templates/email/checking/shared_data_checked.html:20 +#: templates/email/checking/shared_data_checked_team.html:20 +#: templates/email/deleting/shared_data_deleted.html:20 +#: templates/email/deleting/shared_data_deleted_team.html:20 +#: templates/email/other/deduction_changed.html:41 +#: templates/email/other/deduction_changed_team.html:41 +#: templates/email/recording/shared_data_recorded.html:20 +#: templates/email/recording/shared_data_recorded_team.html:20 +#: templates/email/recording/shared_data_unrecorded.html:20 +#: templates/email/recording/shared_data_unrecorded_team.html:20 +#: templates/email/resubmission/resubmission.html:21 +#: templates/email/sharing/shared_access_given.html:21 +#: templates/email/sharing/shared_access_given_team.html:21 +#: templates/email/sharing/shared_access_removed.html:21 +#: templates/email/sharing/shared_access_removed_team.html:21 msgid "Best regards" msgstr "Beste Grüße" @@ -2263,18 +2298,19 @@ msgstr "Freigegebene Daten geprüft" #: templates/email/other/deduction_changed.html:8 #: templates/email/recording/shared_data_recorded.html:8 #: templates/email/recording/shared_data_unrecorded.html:8 +#: templates/email/resubmission/resubmission.html:8 #: templates/email/sharing/shared_access_given.html:8 #: templates/email/sharing/shared_access_removed.html:8 msgid "Hello " msgstr "Hallo " -#: templates/email/checking/shared_data_checked.html:10 -#: templates/email/checking/shared_data_checked_team.html:10 +#: templates/email/checking/shared_data_checked.html:11 +#: templates/email/checking/shared_data_checked_team.html:11 msgid "the following dataset has just been checked" msgstr "der folgende Datensatz wurde soeben geprüft " -#: templates/email/checking/shared_data_checked.html:16 -#: templates/email/checking/shared_data_checked_team.html:16 +#: templates/email/checking/shared_data_checked.html:17 +#: templates/email/checking/shared_data_checked_team.html:17 msgid "" "This means, the responsible registration office just confirmed the " "correctness of this dataset." @@ -2284,6 +2320,7 @@ msgstr "" #: templates/email/checking/shared_data_checked_team.html:8 #: templates/email/deleting/shared_data_deleted_team.html:8 +#: templates/email/other/deduction_changed_team.html:8 #: templates/email/recording/shared_data_recorded_team.html:8 #: templates/email/recording/shared_data_unrecorded_team.html:8 #: templates/email/sharing/shared_access_given_team.html:8 @@ -2296,14 +2333,15 @@ msgstr "Hallo Team" msgid "Shared data deleted" msgstr "Freigegebene Daten gelöscht" -#: templates/email/deleting/shared_data_deleted.html:10 -#: templates/email/deleting/shared_data_deleted_team.html:10 +#: templates/email/deleting/shared_data_deleted.html:11 +#: templates/email/deleting/shared_data_deleted_team.html:11 msgid "the following dataset has just been deleted" msgstr "der folgende Datensatz wurde soeben gelöscht " -#: templates/email/deleting/shared_data_deleted.html:16 -#: templates/email/deleting/shared_data_deleted_team.html:16 -#: templates/email/other/deduction_changed.html:35 +#: templates/email/deleting/shared_data_deleted.html:17 +#: templates/email/deleting/shared_data_deleted_team.html:17 +#: templates/email/other/deduction_changed.html:38 +#: templates/email/other/deduction_changed_team.html:38 msgid "" "If this should not have been happened, please contact us. See the signature " "for details." @@ -2312,27 +2350,33 @@ msgstr "" "mail Signatur finden Sie weitere Kontaktinformationen." #: templates/email/other/deduction_changed.html:4 +#: templates/email/other/deduction_changed_team.html:4 msgid "Deduction changed" msgstr "Abbuchung geändert" -#: templates/email/other/deduction_changed.html:10 +#: templates/email/other/deduction_changed.html:11 +#: templates/email/other/deduction_changed_team.html:11 msgid "a deduction of this eco account has changed:" msgstr "eine Abbuchung des Ökokontos hat sich geändert:" -#: templates/email/other/deduction_changed.html:14 +#: templates/email/other/deduction_changed.html:16 +#: templates/email/other/deduction_changed_team.html:16 msgid "Attribute" msgstr "Attribute" -#: templates/email/other/deduction_changed.html:15 +#: templates/email/other/deduction_changed.html:17 +#: templates/email/other/deduction_changed_team.html:17 msgid "Old" msgstr "Alt" -#: templates/email/other/deduction_changed.html:16 +#: templates/email/other/deduction_changed.html:18 +#: templates/email/other/deduction_changed_team.html:18 #: templates/generic_index.html:43 user/templates/user/team/index.html:22 msgid "New" msgstr "Neu" -#: templates/email/other/deduction_changed.html:19 +#: templates/email/other/deduction_changed.html:21 +#: templates/email/other/deduction_changed_team.html:21 msgid "EcoAccount" msgstr "Ökokonto" @@ -2341,19 +2385,19 @@ msgstr "Ökokonto" msgid "Shared data recorded" msgstr "Freigegebene Daten verzeichnet" -#: templates/email/recording/shared_data_recorded.html:10 -#: templates/email/recording/shared_data_recorded_team.html:10 +#: templates/email/recording/shared_data_recorded.html:11 +#: templates/email/recording/shared_data_recorded_team.html:11 msgid "the following dataset has just been recorded" msgstr "der folgende Datensatz wurde soeben verzeichnet " -#: templates/email/recording/shared_data_recorded.html:16 -#: templates/email/recording/shared_data_recorded_team.html:16 +#: templates/email/recording/shared_data_recorded.html:17 +#: templates/email/recording/shared_data_recorded_team.html:17 msgid "This means the data is now publicly available, e.g. in LANIS" msgstr "" "Das bedeutet, dass die Daten nun öffentlich verfügbar sind, z.B. im LANIS." -#: templates/email/recording/shared_data_recorded.html:26 -#: templates/email/recording/shared_data_recorded_team.html:26 +#: templates/email/recording/shared_data_recorded.html:27 +#: templates/email/recording/shared_data_recorded_team.html:27 msgid "" "Please note: Recorded intervention means the compensations are recorded as " "well." @@ -2366,18 +2410,18 @@ msgstr "" msgid "Shared data unrecorded" msgstr "Freigegebene Daten entzeichnet" -#: templates/email/recording/shared_data_unrecorded.html:10 -#: templates/email/recording/shared_data_unrecorded_team.html:10 +#: templates/email/recording/shared_data_unrecorded.html:11 +#: templates/email/recording/shared_data_unrecorded_team.html:11 msgid "the following dataset has just been unrecorded" msgstr "der folgende Datensatz wurde soeben entzeichnet " -#: templates/email/recording/shared_data_unrecorded.html:16 -#: templates/email/recording/shared_data_unrecorded_team.html:16 +#: templates/email/recording/shared_data_unrecorded.html:17 +#: templates/email/recording/shared_data_unrecorded_team.html:17 msgid "This means the data is no longer publicly available." msgstr "Das bedeutet, dass die Daten nicht länger öffentlich verfügbar sind." -#: templates/email/recording/shared_data_unrecorded.html:26 -#: templates/email/recording/shared_data_unrecorded_team.html:26 +#: templates/email/recording/shared_data_unrecorded.html:27 +#: templates/email/recording/shared_data_unrecorded_team.html:27 msgid "" "Please note: Unrecorded intervention means the compensations are unrecorded " "as well." @@ -2385,22 +2429,30 @@ msgstr "" "Bitte beachten Sie: Entzeichnete Eingriffe bedeuten, dass auch die " "zugehörigen Kompensationen automatisch entzeichnet worden sind." +#: templates/email/resubmission/resubmission.html:11 +msgid "you wanted to be reminded on this entry." +msgstr "Sie wollten an diesen Eintrag erinnert werden." + +#: templates/email/resubmission/resubmission.html:15 +msgid "Your personal comment:" +msgstr "Ihr Kommentar:" + #: templates/email/sharing/shared_access_given.html:4 #: templates/email/sharing/shared_access_given_team.html:4 msgid "Access shared" msgstr "Zugriff freigegeben" -#: templates/email/sharing/shared_access_given.html:10 +#: templates/email/sharing/shared_access_given.html:11 msgid "the following dataset has just been shared with you" msgstr "der folgende Datensatz wurde soeben für Sie freigegeben " -#: templates/email/sharing/shared_access_given.html:16 -#: templates/email/sharing/shared_access_given_team.html:16 +#: templates/email/sharing/shared_access_given.html:17 +#: templates/email/sharing/shared_access_given_team.html:17 msgid "This means you can now edit this dataset." msgstr "Das bedeutet, dass Sie diesen Datensatz nun auch bearbeiten können." -#: templates/email/sharing/shared_access_given.html:17 -#: templates/email/sharing/shared_access_given_team.html:17 +#: templates/email/sharing/shared_access_given.html:18 +#: templates/email/sharing/shared_access_given_team.html:18 msgid "" "The shared dataset appears now by default on your overview for this dataset " "type." @@ -2408,8 +2460,8 @@ msgstr "" "Der freigegebene Datensatz ist nun standardmäßig in Ihrer Übersicht für den " "Datensatztyp im KSP gelistet." -#: templates/email/sharing/shared_access_given.html:27 -#: templates/email/sharing/shared_access_given_team.html:27 +#: templates/email/sharing/shared_access_given.html:28 +#: templates/email/sharing/shared_access_given_team.html:28 msgid "" "Please note: Shared access on an intervention means you automatically have " "editing access to related compensations." @@ -2418,7 +2470,7 @@ msgstr "" "Sie automatisch auch Zugriff auf die zugehörigen Kompensationen erhalten " "haben." -#: templates/email/sharing/shared_access_given_team.html:10 +#: templates/email/sharing/shared_access_given_team.html:11 msgid "the following dataset has just been shared with your team" msgstr "der folgende Datensatz wurde soeben für Ihr Team freigegeben " @@ -2427,20 +2479,20 @@ msgstr "der folgende Datensatz wurde soeben für Ihr Team freigegeben " msgid "Shared access removed" msgstr "Freigegebener Zugriff entzogen" -#: templates/email/sharing/shared_access_removed.html:10 +#: templates/email/sharing/shared_access_removed.html:11 msgid "" "your shared access, including editing, has been revoked for the dataset " msgstr "" "Ihnen wurde soeben der bearbeitende Zugriff auf den folgenden Datensatz " "entzogen: " -#: templates/email/sharing/shared_access_removed.html:16 -#: templates/email/sharing/shared_access_removed_team.html:16 +#: templates/email/sharing/shared_access_removed.html:17 +#: templates/email/sharing/shared_access_removed_team.html:17 msgid "However, you are still able to view the dataset content." msgstr "Sie können den Datensatz aber immer noch im KSP einsehen." -#: templates/email/sharing/shared_access_removed.html:17 -#: templates/email/sharing/shared_access_removed_team.html:17 +#: templates/email/sharing/shared_access_removed.html:18 +#: templates/email/sharing/shared_access_removed_team.html:18 msgid "" "Please use the provided search filter on the dataset`s overview pages to " "find them." @@ -2448,7 +2500,7 @@ msgstr "" "Nutzen Sie hierzu einfach die entsprechenden Suchfilter auf den " "Übersichtsseiten" -#: templates/email/sharing/shared_access_removed_team.html:10 +#: templates/email/sharing/shared_access_removed_team.html:11 msgid "" "your teams shared access, including editing, has been revoked for the " "dataset " From 0bf2051bdf6debb3c508ee06f03c51970d104d2b Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 15 Aug 2022 10:50:01 +0200 Subject: [PATCH 10/48] Migrations + Cleanup * adds needed migrations * refactors forms.py (700+ lines) in main konova app * splits into forms/ and forms/modals and single class/topic-files for better maintainability and overview * fixes bug in main konova app migration which could occur if a certain compensation migration did not run before --- compensation/forms/modalForms.py | 2 +- .../migrations/0008_auto_20220815_0803.py | 24 + .../migrations/0009_auto_20220815_0803.py | 32 + .../migrations/0010_auto_20220815_1030.py | 24 + compensation/views/compensation.py | 3 +- compensation/views/eco_account.py | 5 +- compensation/views/payment.py | 1 - ema/forms.py | 5 +- ema/migrations/0005_ema_resubmission.py | 19 + ema/migrations/0006_auto_20220815_0803.py | 23 + ema/migrations/0007_auto_20220815_1030.py | 19 + ema/views.py | 3 +- intervention/forms/forms.py | 3 +- intervention/forms/modalForms.py | 3 +- .../0005_intervention_resubmission.py | 19 + .../migrations/0006_auto_20220815_0803.py | 23 + .../migrations/0007_auto_20220815_1030.py | 19 + intervention/views.py | 3 +- konova/forms.py | 760 ------------------ konova/forms/__init__.py | 11 + konova/forms/base_form.py | 157 ++++ konova/forms/geometry_form.py | 133 +++ konova/forms/modals/__init__.py | 12 + konova/forms/modals/base_form.py | 73 ++ konova/forms/modals/document_form.py | 163 ++++ konova/forms/modals/record_form.py | 123 +++ konova/forms/modals/remove_form.py | 58 ++ konova/forms/modals/resubmission_form.py | 85 ++ konova/forms/remove_form.py | 54 ++ konova/migrations/0005_auto_20220216_0856.py | 1 + konova/migrations/0014_resubmission.py | 33 + konova/models/object.py | 1 - konova/utils/documents.py | 5 +- user/forms.py | 3 +- user/migrations/0006_auto_20220815_0759.py | 18 + 35 files changed, 1143 insertions(+), 777 deletions(-) create mode 100644 compensation/migrations/0008_auto_20220815_0803.py create mode 100644 compensation/migrations/0009_auto_20220815_0803.py create mode 100644 compensation/migrations/0010_auto_20220815_1030.py create mode 100644 ema/migrations/0005_ema_resubmission.py create mode 100644 ema/migrations/0006_auto_20220815_0803.py create mode 100644 ema/migrations/0007_auto_20220815_1030.py create mode 100644 intervention/migrations/0005_intervention_resubmission.py create mode 100644 intervention/migrations/0006_auto_20220815_0803.py create mode 100644 intervention/migrations/0007_auto_20220815_1030.py delete mode 100644 konova/forms.py create mode 100644 konova/forms/__init__.py create mode 100644 konova/forms/base_form.py create mode 100644 konova/forms/geometry_form.py create mode 100644 konova/forms/modals/__init__.py create mode 100644 konova/forms/modals/base_form.py create mode 100644 konova/forms/modals/document_form.py create mode 100644 konova/forms/modals/record_form.py create mode 100644 konova/forms/modals/remove_form.py create mode 100644 konova/forms/modals/resubmission_form.py create mode 100644 konova/forms/remove_form.py create mode 100644 konova/migrations/0014_resubmission.py create mode 100644 user/migrations/0006_auto_20220815_0759.py diff --git a/compensation/forms/modalForms.py b/compensation/forms/modalForms.py index 581a7b3a..ef21aa3f 100644 --- a/compensation/forms/modalForms.py +++ b/compensation/forms/modalForms.py @@ -20,7 +20,7 @@ from compensation.models import CompensationDocument, EcoAccountDocument from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \ CompensationStateTreeRadioSelect from konova.contexts import BaseContext -from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm +from konova.forms.modals import BaseModalForm, NewDocumentModalForm, RemoveModalForm from konova.models import DeadlineType from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, \ ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED diff --git a/compensation/migrations/0008_auto_20220815_0803.py b/compensation/migrations/0008_auto_20220815_0803.py new file mode 100644 index 00000000..a4d63132 --- /dev/null +++ b/compensation/migrations/0008_auto_20220815_0803.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.3 on 2022-08-15 06:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('konova', '0014_resubmission'), + ('compensation', '0007_auto_20220531_1245'), + ] + + operations = [ + migrations.AddField( + model_name='compensation', + name='resubmission', + field=models.ManyToManyField(blank=True, null=True, related_name='_compensation_resubmission_+', to='konova.Resubmission'), + ), + migrations.AddField( + model_name='ecoaccount', + name='resubmission', + field=models.ManyToManyField(blank=True, null=True, related_name='_ecoaccount_resubmission_+', to='konova.Resubmission'), + ), + ] diff --git a/compensation/migrations/0009_auto_20220815_0803.py b/compensation/migrations/0009_auto_20220815_0803.py new file mode 100644 index 00000000..a7c00e60 --- /dev/null +++ b/compensation/migrations/0009_auto_20220815_0803.py @@ -0,0 +1,32 @@ +# Generated by Django 3.1.3 on 2022-08-15 06:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('konova', '0014_resubmission'), + ('compensation', '0008_auto_20220815_0803'), + ] + + operations = [ + migrations.RemoveField( + model_name='compensation', + name='resubmission', + ), + migrations.RemoveField( + model_name='ecoaccount', + name='resubmission', + ), + migrations.AddField( + model_name='compensation', + name='resubmissions', + field=models.ManyToManyField(blank=True, null=True, related_name='_compensation_resubmissions_+', to='konova.Resubmission'), + ), + migrations.AddField( + model_name='ecoaccount', + name='resubmissions', + field=models.ManyToManyField(blank=True, null=True, related_name='_ecoaccount_resubmissions_+', to='konova.Resubmission'), + ), + ] diff --git a/compensation/migrations/0010_auto_20220815_1030.py b/compensation/migrations/0010_auto_20220815_1030.py new file mode 100644 index 00000000..2d3f16e2 --- /dev/null +++ b/compensation/migrations/0010_auto_20220815_1030.py @@ -0,0 +1,24 @@ +# Generated by Django 3.1.3 on 2022-08-15 08:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('konova', '0014_resubmission'), + ('compensation', '0009_auto_20220815_0803'), + ] + + operations = [ + migrations.AlterField( + model_name='compensation', + name='resubmissions', + field=models.ManyToManyField(blank=True, related_name='_compensation_resubmissions_+', to='konova.Resubmission'), + ), + migrations.AlterField( + model_name='ecoaccount', + name='resubmissions', + field=models.ManyToManyField(blank=True, related_name='_ecoaccount_resubmissions_+', to='konova.Resubmission'), + ), + ] diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py index 016f8ea9..db01a045 100644 --- a/compensation/views/compensation.py +++ b/compensation/views/compensation.py @@ -14,8 +14,9 @@ from compensation.tables import CompensationTable from intervention.models import Intervention from konova.contexts import BaseContext from konova.decorators import * -from konova.forms import RemoveModalForm, SimpleGeomForm, RemoveDeadlineModalForm, EditDocumentModalForm, \ +from konova.forms.modals import RemoveModalForm,RemoveDeadlineModalForm, EditDocumentModalForm, \ ResubmissionModalForm +from konova.forms import SimpleGeomForm from konova.models import Deadline from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.documents import get_document, remove_document diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index 03109a8c..2ebeb1f7 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -25,14 +25,15 @@ from intervention.forms.modalForms import NewDeductionModalForm, ShareModalForm, from konova.contexts import BaseContext from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \ shared_access_required -from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentModalForm, RecordModalForm, \ +from konova.forms.modals import RemoveModalForm, RecordModalForm, \ RemoveDeadlineModalForm, EditDocumentModalForm, ResubmissionModalForm +from konova.forms import SimpleGeomForm from konova.models import Deadline from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.documents import get_document, remove_document from konova.utils.generators import generate_qr_code -from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \ +from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, \ CANCEL_ACC_RECORDED_OR_DEDUCTED, DEDUCTION_REMOVED, DEDUCTION_ADDED, DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, \ COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, \ DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, \ diff --git a/compensation/views/payment.py b/compensation/views/payment.py index 2be5455e..84fad5bc 100644 --- a/compensation/views/payment.py +++ b/compensation/views/payment.py @@ -15,7 +15,6 @@ from compensation.forms.modalForms import NewPaymentForm, RemovePaymentModalForm from compensation.models import Payment from intervention.models import Intervention from konova.decorators import default_group_required, shared_access_required -from konova.forms import RemoveModalForm from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED diff --git a/ema/forms.py b/ema/forms.py index a7e82c4f..93f23490 100644 --- a/ema/forms.py +++ b/ema/forms.py @@ -5,8 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 06.10.21 """ -from dal import autocomplete -from django import forms from user.models import User from django.db import transaction from django.urls import reverse, reverse_lazy @@ -16,7 +14,8 @@ from compensation.forms.forms import AbstractCompensationForm, CompensationRespo PikCompensationFormMixin from ema.models import Ema, EmaDocument from intervention.models import Responsibility, Handler -from konova.forms import SimpleGeomForm, NewDocumentModalForm +from konova.forms import SimpleGeomForm +from konova.forms.modals import NewDocumentModalForm from user.models import UserActionLogEntry diff --git a/ema/migrations/0005_ema_resubmission.py b/ema/migrations/0005_ema_resubmission.py new file mode 100644 index 00000000..57a1fbe7 --- /dev/null +++ b/ema/migrations/0005_ema_resubmission.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.3 on 2022-08-15 06:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('konova', '0014_resubmission'), + ('ema', '0004_ema_is_pik'), + ] + + operations = [ + migrations.AddField( + model_name='ema', + name='resubmission', + field=models.ManyToManyField(blank=True, null=True, related_name='_ema_resubmission_+', to='konova.Resubmission'), + ), + ] diff --git a/ema/migrations/0006_auto_20220815_0803.py b/ema/migrations/0006_auto_20220815_0803.py new file mode 100644 index 00000000..44ae7657 --- /dev/null +++ b/ema/migrations/0006_auto_20220815_0803.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.3 on 2022-08-15 06:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('konova', '0014_resubmission'), + ('ema', '0005_ema_resubmission'), + ] + + operations = [ + migrations.RemoveField( + model_name='ema', + name='resubmission', + ), + migrations.AddField( + model_name='ema', + name='resubmissions', + field=models.ManyToManyField(blank=True, null=True, related_name='_ema_resubmissions_+', to='konova.Resubmission'), + ), + ] diff --git a/ema/migrations/0007_auto_20220815_1030.py b/ema/migrations/0007_auto_20220815_1030.py new file mode 100644 index 00000000..84429174 --- /dev/null +++ b/ema/migrations/0007_auto_20220815_1030.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.3 on 2022-08-15 08:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('konova', '0014_resubmission'), + ('ema', '0006_auto_20220815_0803'), + ] + + operations = [ + migrations.AlterField( + model_name='ema', + name='resubmissions', + field=models.ManyToManyField(blank=True, related_name='_ema_resubmissions_+', to='konova.Resubmission'), + ), + ] diff --git a/ema/views.py b/ema/views.py index 9cd6dd9d..f07187aa 100644 --- a/ema/views.py +++ b/ema/views.py @@ -16,8 +16,9 @@ from intervention.forms.modalForms import ShareModalForm from konova.contexts import BaseContext from konova.decorators import conservation_office_group_required, shared_access_required from ema.models import Ema, EmaDocument -from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm, RemoveDeadlineModalForm, \ +from konova.forms.modals import RemoveModalForm, RecordModalForm, RemoveDeadlineModalForm, \ EditDocumentModalForm, ResubmissionModalForm +from konova.forms import SimpleGeomForm from konova.models import Deadline from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER diff --git a/intervention/forms/forms.py b/intervention/forms/forms.py index b85ba101..15b02fd7 100644 --- a/intervention/forms/forms.py +++ b/intervention/forms/forms.py @@ -8,6 +8,7 @@ Created on: 02.12.20 from dal import autocomplete from django import forms +from konova.forms.base_form import BaseForm from konova.utils.message_templates import EDITED_GENERAL_DATA from user.models import User from django.db import transaction @@ -19,7 +20,7 @@ from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \ CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID from intervention.inputs import GenerateInput from intervention.models import Intervention, Legal, Responsibility, Handler -from konova.forms import BaseForm, SimpleGeomForm +from konova.forms.geometry_form import SimpleGeomForm from user.models import UserActionLogEntry diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py index b6445a55..a977c1ce 100644 --- a/intervention/forms/modalForms.py +++ b/intervention/forms/modalForms.py @@ -19,7 +19,8 @@ from django.utils.translation import gettext_lazy as _ from compensation.models import EcoAccount, EcoAccountDeduction from intervention.inputs import TextToClipboardInput from intervention.models import Intervention, InterventionDocument, RevocationDocument -from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm +from konova.forms.modals import BaseModalForm +from konova.forms.modals import NewDocumentModalForm, RemoveModalForm from konova.utils.general import format_german_float from konova.utils.user_checks import is_default_group_only diff --git a/intervention/migrations/0005_intervention_resubmission.py b/intervention/migrations/0005_intervention_resubmission.py new file mode 100644 index 00000000..ac489238 --- /dev/null +++ b/intervention/migrations/0005_intervention_resubmission.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.3 on 2022-08-15 06:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('konova', '0014_resubmission'), + ('intervention', '0004_auto_20220303_0956'), + ] + + operations = [ + migrations.AddField( + model_name='intervention', + name='resubmission', + field=models.ManyToManyField(blank=True, null=True, related_name='_intervention_resubmission_+', to='konova.Resubmission'), + ), + ] diff --git a/intervention/migrations/0006_auto_20220815_0803.py b/intervention/migrations/0006_auto_20220815_0803.py new file mode 100644 index 00000000..8d0bf80d --- /dev/null +++ b/intervention/migrations/0006_auto_20220815_0803.py @@ -0,0 +1,23 @@ +# Generated by Django 3.1.3 on 2022-08-15 06:03 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('konova', '0014_resubmission'), + ('intervention', '0005_intervention_resubmission'), + ] + + operations = [ + migrations.RemoveField( + model_name='intervention', + name='resubmission', + ), + migrations.AddField( + model_name='intervention', + name='resubmissions', + field=models.ManyToManyField(blank=True, null=True, related_name='_intervention_resubmissions_+', to='konova.Resubmission'), + ), + ] diff --git a/intervention/migrations/0007_auto_20220815_1030.py b/intervention/migrations/0007_auto_20220815_1030.py new file mode 100644 index 00000000..b7a2729d --- /dev/null +++ b/intervention/migrations/0007_auto_20220815_1030.py @@ -0,0 +1,19 @@ +# Generated by Django 3.1.3 on 2022-08-15 08:30 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('konova', '0014_resubmission'), + ('intervention', '0006_auto_20220815_0803'), + ] + + operations = [ + migrations.AlterField( + model_name='intervention', + name='resubmissions', + field=models.ManyToManyField(blank=True, related_name='_intervention_resubmissions_+', to='konova.Resubmission'), + ), + ] diff --git a/intervention/views.py b/intervention/views.py index c55fe722..6a9304e9 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -12,7 +12,8 @@ from intervention.models import Intervention, Revocation, InterventionDocument, from intervention.tables import InterventionTable from konova.contexts import BaseContext from konova.decorators import * -from konova.forms import SimpleGeomForm, RemoveModalForm, RecordModalForm, EditDocumentModalForm, ResubmissionModalForm +from konova.forms import SimpleGeomForm +from konova.forms.modals import RemoveModalForm, RecordModalForm, EditDocumentModalForm, ResubmissionModalForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.documents import remove_document, get_document from konova.utils.generators import generate_qr_code diff --git a/konova/forms.py b/konova/forms.py deleted file mode 100644 index 5d8f38e2..00000000 --- a/konova/forms.py +++ /dev/null @@ -1,760 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 16.11.20 - -""" -import json -from abc import abstractmethod - -from bootstrap_modal_forms.forms import BSModalForm -from bootstrap_modal_forms.utils import is_ajax -from django import forms -from django.contrib import messages -from django.contrib.gis import gdal -from django.core.exceptions import ObjectDoesNotExist -from django.db.models.fields.files import FieldFile -from django.utils.timezone import now - -from compensation.models import EcoAccount -from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP -from user.models import User -from django.contrib.gis.forms import MultiPolygonField -from django.contrib.gis.geos import MultiPolygon, Polygon -from django.db import transaction -from django.http import HttpRequest, HttpResponseRedirect -from django.shortcuts import render -from django.utils.translation import gettext_lazy as _ - -from konova.contexts import BaseContext -from konova.models import BaseObject, Geometry, RecordableObjectMixin, AbstractDocument, Resubmission -from konova.settings import DEFAULT_SRID -from konova.tasks import celery_update_parcels -from konova.utils.message_templates import FORM_INVALID, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE, DOCUMENT_EDITED -from user.models import UserActionLogEntry - - -class BaseForm(forms.Form): - """ - Basic form for that holds attributes needed in all other forms - """ - template = None - action_url = None - action_btn_label = _("Save") - form_title = None - cancel_redirect = None - form_caption = None - instance = None # The data holding model object - request = None - form_attrs = {} # Holds additional attributes, that can be used in the template - has_required_fields = False # Automatically set. Triggers hint rendering in templates - show_cancel_btn = True - - def __init__(self, *args, **kwargs): - self.instance = kwargs.pop("instance", None) - super().__init__(*args, **kwargs) - if self.request is not None: - self.user = self.request.user - # Check for required fields - for _field_name, _field_val in self.fields.items(): - if _field_val.required: - self.has_required_fields = True - break - - self.check_for_recorded_instance() - - @abstractmethod - def save(self): - # To be implemented in subclasses! - pass - - def disable_form_field(self, field: str): - """ - Disables a form field for user editing - """ - self.fields[field].widget.attrs["readonly"] = True - self.fields[field].disabled = True - self.fields[field].widget.attrs["title"] = _("Not editable") - - def initialize_form_field(self, field: str, val): - """ - Initializes a form field with a value - """ - self.fields[field].initial = val - - def add_placeholder_for_field(self, field: str, val): - """ - Adds a placeholder to a field after initialization without the need to redefine the form widget - - Args: - field (str): Field name - val (str): Placeholder - - Returns: - - """ - self.fields[field].widget.attrs["placeholder"] = val - - def load_initial_data(self, form_data: dict, disabled_fields: list = None): - """ Initializes form data from instance - - Inserts instance data into form and disables form fields - - Returns: - - """ - if self.instance is None: - return - for k, v in form_data.items(): - self.initialize_form_field(k, v) - if disabled_fields: - for field in disabled_fields: - self.disable_form_field(field) - - def add_widget_html_class(self, field: str, cls: str): - """ Adds a HTML class string to the widget of a field - - Args: - field (str): The field's name - cls (str): The new class string - - Returns: - - """ - set_class = self.fields[field].widget.attrs.get("class", "") - if cls in set_class: - return - else: - set_class += " " + cls - self.fields[field].widget.attrs["class"] = set_class - - def remove_widget_html_class(self, field: str, cls: str): - """ Removes a HTML class string from the widget of a field - - Args: - field (str): The field's name - cls (str): The new class string - - Returns: - - """ - set_class = self.fields[field].widget.attrs.get("class", "") - set_class = set_class.replace(cls, "") - self.fields[field].widget.attrs["class"] = set_class - - def check_for_recorded_instance(self): - """ Checks if the instance is recorded and runs some special logic if yes - - If the instance is recorded, the form shall not display any possibility to - edit any data. Instead, the users should get some information about why they can not edit anything. - - There are situations where the form should be rendered regularly, - e.g deduction forms for (recorded) eco accounts. - - Returns: - - """ - from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \ - RemoveEcoAccountDeductionModalForm - is_none = self.instance is None - is_other_data_type = not isinstance(self.instance, BaseObject) - is_deduction_form_from_account = isinstance( - self, - ( - NewDeductionModalForm, - ResubmissionModalForm, - EditEcoAccountDeductionModalForm, - RemoveEcoAccountDeductionModalForm, - ) - ) and isinstance(self.instance, EcoAccount) - - if is_none or is_other_data_type or is_deduction_form_from_account: - # Do nothing - return - - if self.instance.is_recorded: - self.template = "form/recorded_no_edit.html" - - -class RemoveForm(BaseForm): - check = forms.BooleanField( - label=_("Confirm"), - label_suffix=_(""), - required=True, - ) - - def __init__(self, *args, **kwargs): - self.object_to_remove = kwargs.pop("object_to_remove", None) - self.remove_post_url = kwargs.pop("remove_post_url", "") - self.cancel_url = kwargs.pop("cancel_url", "") - - super().__init__(*args, **kwargs) - - self.form_title = _("Remove") - if self.object_to_remove is not None: - self.form_caption = _("You are about to remove {} {}").format(self.object_to_remove.__class__.__name__, self.object_to_remove) - self.action_url = self.remove_post_url - self.cancel_redirect = self.cancel_url - - def is_checked(self) -> bool: - return self.cleaned_data.get("check", False) - - def save(self, user: User): - """ Perform generic removing by running the form typical 'save()' method - - Args: - user (User): The performing user - - Returns: - - """ - if self.object_to_remove is not None and self.is_checked(): - with transaction.atomic(): - self.object_to_remove.is_active = False - action = UserActionLogEntry.get_deleted_action(user) - self.object_to_remove.deleted = action - self.object_to_remove.save() - return self.object_to_remove - - -class BaseModalForm(BaseForm, BSModalForm): - """ A specialzed form class for modal form handling - - """ - is_modal_form = True - render_submit = True - template = "modal/modal_form.html" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.action_btn_label = _("Continue") - - def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None): - """ Generic processing of request - - Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used - - Args: - request (HttpRequest): The incoming request - msg_success (str): The message in case of successful removing - msg_error (str): The message in case of an error - - Returns: - - """ - redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home") - template = self.template - if request.method == "POST": - if self.is_valid(): - if not is_ajax(request.META): - # Modal forms send one POST for checking on data validity. This can be used to return possible errors - # on the form. A second POST (if no errors occured) is sent afterwards and needs to process the - # saving/commiting of the data to the database. is_ajax() performs this check. The first request is - # an ajax call, the second is a regular form POST. - self.save() - messages.success( - request, - msg_success - ) - return HttpResponseRedirect(redirect_url) - else: - context = { - "form": self, - } - context = BaseContext(request, context).context - return render(request, template, context) - elif request.method == "GET": - context = { - "form": self, - } - context = BaseContext(request, context).context - return render(request, template, context) - else: - raise NotImplementedError - - -class SimpleGeomForm(BaseForm): - """ A geometry form for rendering geometry read-only using a widget - - """ - read_only = True - geom = MultiPolygonField( - srid=DEFAULT_SRID_RLP, - label=_("Geometry"), - help_text=_(""), - label_suffix="", - required=False, - disabled=False, - ) - - def __init__(self, *args, **kwargs): - self.read_only = kwargs.pop("read_only", True) - super().__init__(*args, **kwargs) - - # Initialize geometry - try: - geom = self.instance.geometry.geom - self.empty = geom.empty - - if self.empty: - raise AttributeError - - geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP) - geom = json.dumps(geojson) - except AttributeError: - # If no geometry exists for this form, we simply set the value to None and zoom to the maximum level - geom = "" - self.empty = True - - self.initialize_form_field("geom", geom) - - def is_valid(self): - super().is_valid() - is_valid = True - - # Get geojson from form - geom = self.data["geom"] - if geom is None or len(geom) == 0: - # empty geometry is a valid geometry - return is_valid - geom = json.loads(geom) - - # Write submitted data back into form field to make sure invalid geometry - # will be rendered again on failed submit - self.initialize_form_field("geom", self.data["geom"]) - - # Read geojson into gdal geometry - # HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for - # this case) - features = [] - features_json = geom.get("features", []) - for feature in features_json: - g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP) - if g.geom_type not in ["Polygon", "MultiPolygon"]: - self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered.")) - is_valid = False - return is_valid - - polygon = Polygon.from_ewkt(g.ewkt) - is_valid = polygon.valid - if not is_valid: - self.add_error("geom", polygon.valid_reason) - return is_valid - - features.append(polygon) - form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP) - for feature in features: - form_geom = form_geom.union(feature) - - # Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided. - if form_geom.geom_type != "MultiPolygon": - form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP) - - # Write unioned Multipolygon into cleaned data - if self.cleaned_data is None: - self.cleaned_data = {} - self.cleaned_data["geom"] = form_geom.ewkt - - return is_valid - - def save(self, action: UserActionLogEntry): - """ Saves the form's geometry - - Creates a new geometry entry if none is set, yet - - Args: - action (): - - Returns: - - """ - try: - if self.instance is None or self.instance.geometry is None: - raise LookupError - geometry = self.instance.geometry - geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)) - geometry.modified = action - - geometry.save() - except LookupError: - # No geometry or linked instance holding a geometry exist --> create a new one! - geometry = Geometry.objects.create( - geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)), - created=action, - ) - # Start the parcel update procedure in a background process - celery_update_parcels.delay(geometry.id) - return geometry - - -class RemoveModalForm(BaseModalForm): - """ Generic removing modal form - - Can be used for anything, where removing shall be confirmed by the user a second time. - - """ - confirm = forms.BooleanField( - label=_("Confirm"), - label_suffix=_(""), - widget=forms.CheckboxInput(), - required=True, - ) - - def __init__(self, *args, **kwargs): - self.template = "modal/modal_form.html" - super().__init__(*args, **kwargs) - self.form_title = _("Remove") - self.form_caption = _("Are you sure?") - # Disable automatic w-100 setting for this type of modal form. Looks kinda strange - self.fields["confirm"].widget.attrs["class"] = "" - - def save(self): - if isinstance(self.instance, BaseObject): - self.instance.mark_as_deleted(self.user) - else: - # If the class does not provide restorable delete functionality, we must delete the entry finally - self.instance.delete() - - -class RemoveDeadlineModalForm(RemoveModalForm): - """ Removing modal form for deadlines - - Can be used for anything, where removing shall be confirmed by the user a second time. - - """ - deadline = None - - def __init__(self, *args, **kwargs): - deadline = kwargs.pop("deadline", None) - self.deadline = deadline - super().__init__(*args, **kwargs) - - def save(self): - self.instance.remove_deadline(self) - - -class NewDocumentModalForm(BaseModalForm): - """ Modal form for new documents - - """ - title = forms.CharField( - label=_("Title"), - label_suffix=_(""), - max_length=500, - widget=forms.TextInput( - attrs={ - "class": "form-control", - } - ) - ) - creation_date = forms.DateField( - label=_("Created on"), - label_suffix=_(""), - help_text=_("When has this file been created? Important for photos."), - widget=forms.DateInput( - attrs={ - "type": "date", - "data-provide": "datepicker", - "class": "form-control", - }, - format="%d.%m.%Y" - ) - ) - file = forms.FileField( - label=_("File"), - label_suffix=_(""), - help_text=_("Allowed formats: pdf, jpg, png. Max size 15 MB."), - widget=forms.FileInput( - attrs={ - "class": "form-control-file", - } - ), - ) - comment = forms.CharField( - required=False, - max_length=200, - label=_("Comment"), - label_suffix=_(""), - help_text=_("Additional comment, maximum {} letters").format(200), - widget=forms.Textarea( - attrs={ - "cols": 30, - "rows": 5, - "class": "form-control", - } - ) - ) - document_model = None - - class Meta: - abstract = True - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Add new document") - self.form_caption = _("") - self.form_attrs = { - "enctype": "multipart/form-data", # important for file upload - } - if not self.document_model: - raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__)) - - def is_valid(self): - super_valid = super().is_valid() - - _file = self.cleaned_data.get("file", None) - - if _file is None or isinstance(_file, FieldFile): - # FieldFile declares that no new file has been uploaded and we do not need to check on the file again - return super_valid - - mime_type_valid = self.document_model.is_mime_type_valid(_file) - if not mime_type_valid: - self.add_error( - "file", - FILE_TYPE_UNSUPPORTED - ) - - file_size_valid = self.document_model.is_file_size_valid(_file) - if not file_size_valid: - self.add_error( - "file", - FILE_SIZE_TOO_LARGE - ) - - file_valid = mime_type_valid and file_size_valid - return super_valid and file_valid - - def save(self): - with transaction.atomic(): - action = UserActionLogEntry.get_created_action(self.user) - edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document")) - - doc = self.document_model.objects.create( - created=action, - title=self.cleaned_data["title"], - comment=self.cleaned_data["comment"], - file=self.cleaned_data["file"], - date_of_creation=self.cleaned_data["creation_date"], - instance=self.instance, - ) - - self.instance.log.add(edited_action) - self.instance.modified = edited_action - self.instance.save() - - return doc - - -class EditDocumentModalForm(NewDocumentModalForm): - document = None - document_model = AbstractDocument - - def __init__(self, *args, **kwargs): - self.document = kwargs.pop("document", None) - super().__init__(*args, **kwargs) - self.form_title = _("Edit document") - form_data = { - "title": self.document.title, - "comment": self.document.comment, - "creation_date": str(self.document.date_of_creation), - "file": self.document.file, - } - self.load_initial_data(form_data) - - - def save(self): - with transaction.atomic(): - document = self.document - file = self.cleaned_data.get("file", None) - - document.title = self.cleaned_data.get("title", None) - document.comment = self.cleaned_data.get("comment", None) - document.date_of_creation = self.cleaned_data.get("creation_date", None) - if not isinstance(file, FieldFile): - document.replace_file(file) - document.save() - - self.instance.mark_as_edited(self.user, self.request, edit_comment=DOCUMENT_EDITED) - - return document - - -class RecordModalForm(BaseModalForm): - """ Modal form for recording data - - """ - confirm = forms.BooleanField( - label=_("Confirm record"), - label_suffix="", - widget=forms.CheckboxInput(), - required=True, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Record data") - self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) - # Disable automatic w-100 setting for this type of modal form. Looks kinda strange - self.fields["confirm"].widget.attrs["class"] = "" - - if self.instance.recorded: - # unrecord! - self.fields["confirm"].label = _("Confirm unrecord") - self.form_title = _("Unrecord data") - self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name) - - if not isinstance(self.instance, RecordableObjectMixin): - raise NotImplementedError - - def is_valid(self): - """ Checks for instance's validity and data quality - - Returns: - - """ - from intervention.models import Intervention - super_val = super().is_valid() - if self.instance.recorded: - # If user wants to unrecord an already recorded dataset, we do not need to perform custom checks - return super_val - checker = self.instance.quality_check() - for msg in checker.messages: - self.add_error( - "confirm", - msg - ) - valid = checker.valid - # Special case: Intervention - # Add direct checks for related compensations - if isinstance(self.instance, Intervention): - comps_valid = self._are_compensations_valid() - valid = valid and comps_valid - return super_val and valid - - def _are_deductions_valid(self): - """ Performs validity checks on deductions and their eco-account - - Returns: - - """ - deductions = self.instance.deductions.all() - for deduction in deductions: - checker = deduction.account.quality_check() - for msg in checker.messages: - self.add_error( - "confirm", - f"{deduction.account.identifier}: {msg}" - ) - return checker.valid - return True - - def _are_compensations_valid(self): - """ Runs a special case for intervention-compensations validity - - Returns: - - """ - comps = self.instance.compensations.filter( - deleted=None, - ) - comps_valid = True - for comp in comps: - checker = comp.quality_check() - comps_valid = comps_valid and checker.valid - for msg in checker.messages: - self.add_error( - "confirm", - f"{comp.identifier}: {msg}" - ) - - deductions_valid = self._are_deductions_valid() - - return comps_valid and deductions_valid - - def save(self): - with transaction.atomic(): - if self.cleaned_data["confirm"]: - if self.instance.recorded: - self.instance.set_unrecorded(self.user) - else: - self.instance.set_recorded(self.user) - return self.instance - - def check_for_recorded_instance(self): - """ Overwrite the check method for doing nothing on the RecordModalForm - - Returns: - - """ - pass - - -class ResubmissionModalForm(BaseModalForm): - date = forms.DateField( - label_suffix=_(""), - label=_("Date"), - help_text=_("When do you want to be reminded?"), - widget=forms.DateInput( - attrs={ - "type": "date", - "data-provide": "datepicker", - "class": "form-control", - }, - format="%d.%m.%Y" - ) - ) - comment = forms.CharField( - required=False, - label=_("Comment"), - label_suffix=_(""), - help_text=_("Additional comment"), - widget=forms.Textarea( - attrs={ - "cols": 30, - "rows": 5, - "class": "form-control", - } - ) - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Resubmission") - self.form_caption = _("Set your resubmission for this entry.") - self.action_url = None - - try: - self.resubmission = self.instance.resubmissions.get( - user=self.user - ) - self.initialize_form_field("date", str(self.resubmission.resubmit_on)) - self.initialize_form_field("comment", self.resubmission.comment) - except ObjectDoesNotExist: - self.resubmission = Resubmission() - - def is_valid(self): - super_valid = super().is_valid() - self_valid = True - - date = self.cleaned_data.get("date") - today = now().today().date() - if date <= today: - self.add_error( - "date", - _("The date should be in the future") - ) - self_valid = False - - return super_valid and self_valid - - def save(self): - with transaction.atomic(): - self.resubmission.user = self.user - self.resubmission.resubmit_on = self.cleaned_data.get("date") - self.resubmission.comment = self.cleaned_data.get("comment") - self.resubmission.save() - self.instance.resubmissions.add(self.resubmission) - return self.resubmission - diff --git a/konova/forms/__init__.py b/konova/forms/__init__.py new file mode 100644 index 00000000..5840c4d2 --- /dev/null +++ b/konova/forms/__init__.py @@ -0,0 +1,11 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 15.08.22 + +""" + +from .base_form import * +from .geometry_form import * +from .remove_form import * \ No newline at end of file diff --git a/konova/forms/base_form.py b/konova/forms/base_form.py new file mode 100644 index 00000000..065fba17 --- /dev/null +++ b/konova/forms/base_form.py @@ -0,0 +1,157 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 15.08.22 + +""" +from abc import abstractmethod + +from django import forms +from django.utils.translation import gettext_lazy as _ + +from compensation.models import EcoAccount +from konova.models import BaseObject + + +class BaseForm(forms.Form): + """ + Basic form for that holds attributes needed in all other forms + """ + template = None + action_url = None + action_btn_label = _("Save") + form_title = None + cancel_redirect = None + form_caption = None + instance = None # The data holding model object + request = None + form_attrs = {} # Holds additional attributes, that can be used in the template + has_required_fields = False # Automatically set. Triggers hint rendering in templates + show_cancel_btn = True + + def __init__(self, *args, **kwargs): + self.instance = kwargs.pop("instance", None) + super().__init__(*args, **kwargs) + if self.request is not None: + self.user = self.request.user + # Check for required fields + for _field_name, _field_val in self.fields.items(): + if _field_val.required: + self.has_required_fields = True + break + + self.check_for_recorded_instance() + + @abstractmethod + def save(self): + # To be implemented in subclasses! + pass + + def disable_form_field(self, field: str): + """ + Disables a form field for user editing + """ + self.fields[field].widget.attrs["readonly"] = True + self.fields[field].disabled = True + self.fields[field].widget.attrs["title"] = _("Not editable") + + def initialize_form_field(self, field: str, val): + """ + Initializes a form field with a value + """ + self.fields[field].initial = val + + def add_placeholder_for_field(self, field: str, val): + """ + Adds a placeholder to a field after initialization without the need to redefine the form widget + + Args: + field (str): Field name + val (str): Placeholder + + Returns: + + """ + self.fields[field].widget.attrs["placeholder"] = val + + def load_initial_data(self, form_data: dict, disabled_fields: list = None): + """ Initializes form data from instance + + Inserts instance data into form and disables form fields + + Returns: + + """ + if self.instance is None: + return + for k, v in form_data.items(): + self.initialize_form_field(k, v) + if disabled_fields: + for field in disabled_fields: + self.disable_form_field(field) + + def add_widget_html_class(self, field: str, cls: str): + """ Adds a HTML class string to the widget of a field + + Args: + field (str): The field's name + cls (str): The new class string + + Returns: + + """ + set_class = self.fields[field].widget.attrs.get("class", "") + if cls in set_class: + return + else: + set_class += " " + cls + self.fields[field].widget.attrs["class"] = set_class + + def remove_widget_html_class(self, field: str, cls: str): + """ Removes a HTML class string from the widget of a field + + Args: + field (str): The field's name + cls (str): The new class string + + Returns: + + """ + set_class = self.fields[field].widget.attrs.get("class", "") + set_class = set_class.replace(cls, "") + self.fields[field].widget.attrs["class"] = set_class + + def check_for_recorded_instance(self): + """ Checks if the instance is recorded and runs some special logic if yes + + If the instance is recorded, the form shall not display any possibility to + edit any data. Instead, the users should get some information about why they can not edit anything. + + There are situations where the form should be rendered regularly, + e.g deduction forms for (recorded) eco accounts. + + Returns: + + """ + from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \ + RemoveEcoAccountDeductionModalForm + from konova.forms.modals.resubmission_form import ResubmissionModalForm + is_none = self.instance is None + is_other_data_type = not isinstance(self.instance, BaseObject) + is_deduction_form_from_account = isinstance( + self, + ( + NewDeductionModalForm, + ResubmissionModalForm, + EditEcoAccountDeductionModalForm, + RemoveEcoAccountDeductionModalForm, + ) + ) and isinstance(self.instance, EcoAccount) + + if is_none or is_other_data_type or is_deduction_form_from_account: + # Do nothing + return + + if self.instance.is_recorded: + self.template = "form/recorded_no_edit.html" diff --git a/konova/forms/geometry_form.py b/konova/forms/geometry_form.py new file mode 100644 index 00000000..3c957aa7 --- /dev/null +++ b/konova/forms/geometry_form.py @@ -0,0 +1,133 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 15.08.22 + +""" +import json + +from django.contrib.gis import gdal +from django.contrib.gis.forms import MultiPolygonField +from django.contrib.gis.geos import MultiPolygon, Polygon +from django.utils.translation import gettext_lazy as _ + +from konova.forms.base_form import BaseForm +from konova.models import Geometry +from konova.tasks import celery_update_parcels +from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP +from user.models import UserActionLogEntry + + +class SimpleGeomForm(BaseForm): + """ A geometry form for rendering geometry read-only using a widget + + """ + read_only = True + geom = MultiPolygonField( + srid=DEFAULT_SRID_RLP, + label=_("Geometry"), + help_text=_(""), + label_suffix="", + required=False, + disabled=False, + ) + + def __init__(self, *args, **kwargs): + self.read_only = kwargs.pop("read_only", True) + super().__init__(*args, **kwargs) + + # Initialize geometry + try: + geom = self.instance.geometry.geom + self.empty = geom.empty + + if self.empty: + raise AttributeError + + geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP) + geom = json.dumps(geojson) + except AttributeError: + # If no geometry exists for this form, we simply set the value to None and zoom to the maximum level + geom = "" + self.empty = True + + self.initialize_form_field("geom", geom) + + def is_valid(self): + super().is_valid() + is_valid = True + + # Get geojson from form + geom = self.data["geom"] + if geom is None or len(geom) == 0: + # empty geometry is a valid geometry + return is_valid + geom = json.loads(geom) + + # Write submitted data back into form field to make sure invalid geometry + # will be rendered again on failed submit + self.initialize_form_field("geom", self.data["geom"]) + + # Read geojson into gdal geometry + # HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for + # this case) + features = [] + features_json = geom.get("features", []) + for feature in features_json: + g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP) + if g.geom_type not in ["Polygon", "MultiPolygon"]: + self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered.")) + is_valid = False + return is_valid + + polygon = Polygon.from_ewkt(g.ewkt) + is_valid = polygon.valid + if not is_valid: + self.add_error("geom", polygon.valid_reason) + return is_valid + + features.append(polygon) + form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP) + for feature in features: + form_geom = form_geom.union(feature) + + # Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided. + if form_geom.geom_type != "MultiPolygon": + form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP) + + # Write unioned Multipolygon into cleaned data + if self.cleaned_data is None: + self.cleaned_data = {} + self.cleaned_data["geom"] = form_geom.ewkt + + return is_valid + + def save(self, action: UserActionLogEntry): + """ Saves the form's geometry + + Creates a new geometry entry if none is set, yet + + Args: + action (): + + Returns: + + """ + try: + if self.instance is None or self.instance.geometry is None: + raise LookupError + geometry = self.instance.geometry + geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)) + geometry.modified = action + + geometry.save() + except LookupError: + # No geometry or linked instance holding a geometry exist --> create a new one! + geometry = Geometry.objects.create( + geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)), + created=action, + ) + # Start the parcel update procedure in a background process + celery_update_parcels.delay(geometry.id) + return geometry \ No newline at end of file diff --git a/konova/forms/modals/__init__.py b/konova/forms/modals/__init__.py new file mode 100644 index 00000000..f922f2de --- /dev/null +++ b/konova/forms/modals/__init__.py @@ -0,0 +1,12 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 15.08.22 + +""" +from .base_form import * +from .document_form import * +from .record_form import * +from .remove_form import * +from .resubmission_form import * diff --git a/konova/forms/modals/base_form.py b/konova/forms/modals/base_form.py new file mode 100644 index 00000000..a6806578 --- /dev/null +++ b/konova/forms/modals/base_form.py @@ -0,0 +1,73 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 15.08.22 + +""" +from bootstrap_modal_forms.forms import BSModalForm +from bootstrap_modal_forms.utils import is_ajax +from django.contrib import messages +from django.http import HttpResponseRedirect, HttpRequest +from django.shortcuts import render +from django.utils.translation import gettext_lazy as _ + +from konova.contexts import BaseContext +from konova.forms.base_form import BaseForm +from konova.utils.message_templates import FORM_INVALID + + +class BaseModalForm(BaseForm, BSModalForm): + """ A specialzed form class for modal form handling + + """ + is_modal_form = True + render_submit = True + template = "modal/modal_form.html" + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.action_btn_label = _("Continue") + + def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None): + """ Generic processing of request + + Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used + + Args: + request (HttpRequest): The incoming request + msg_success (str): The message in case of successful removing + msg_error (str): The message in case of an error + + Returns: + + """ + redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home") + template = self.template + if request.method == "POST": + if self.is_valid(): + if not is_ajax(request.META): + # Modal forms send one POST for checking on data validity. This can be used to return possible errors + # on the form. A second POST (if no errors occured) is sent afterwards and needs to process the + # saving/commiting of the data to the database. is_ajax() performs this check. The first request is + # an ajax call, the second is a regular form POST. + self.save() + messages.success( + request, + msg_success + ) + return HttpResponseRedirect(redirect_url) + else: + context = { + "form": self, + } + context = BaseContext(request, context).context + return render(request, template, context) + elif request.method == "GET": + context = { + "form": self, + } + context = BaseContext(request, context).context + return render(request, template, context) + else: + raise NotImplementedError diff --git a/konova/forms/modals/document_form.py b/konova/forms/modals/document_form.py new file mode 100644 index 00000000..96b4f8e8 --- /dev/null +++ b/konova/forms/modals/document_form.py @@ -0,0 +1,163 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 15.08.22 + +""" +from django import forms +from django.db import transaction +from django.db.models.fields.files import FieldFile +from django.utils.translation import gettext_lazy as _ + +from konova.forms.modals.base_form import BaseModalForm +from konova.models import AbstractDocument +from konova.utils.message_templates import DOCUMENT_EDITED, FILE_SIZE_TOO_LARGE, FILE_TYPE_UNSUPPORTED +from user.models import UserActionLogEntry + + +class NewDocumentModalForm(BaseModalForm): + """ Modal form for new documents + + """ + title = forms.CharField( + label=_("Title"), + label_suffix=_(""), + max_length=500, + widget=forms.TextInput( + attrs={ + "class": "form-control", + } + ) + ) + creation_date = forms.DateField( + label=_("Created on"), + label_suffix=_(""), + help_text=_("When has this file been created? Important for photos."), + widget=forms.DateInput( + attrs={ + "type": "date", + "data-provide": "datepicker", + "class": "form-control", + }, + format="%d.%m.%Y" + ) + ) + file = forms.FileField( + label=_("File"), + label_suffix=_(""), + help_text=_("Allowed formats: pdf, jpg, png. Max size 15 MB."), + widget=forms.FileInput( + attrs={ + "class": "form-control-file", + } + ), + ) + comment = forms.CharField( + required=False, + max_length=200, + label=_("Comment"), + label_suffix=_(""), + help_text=_("Additional comment, maximum {} letters").format(200), + widget=forms.Textarea( + attrs={ + "cols": 30, + "rows": 5, + "class": "form-control", + } + ) + ) + document_model = None + + class Meta: + abstract = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Add new document") + self.form_caption = _("") + self.form_attrs = { + "enctype": "multipart/form-data", # important for file upload + } + if not self.document_model: + raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__)) + + def is_valid(self): + super_valid = super().is_valid() + + _file = self.cleaned_data.get("file", None) + + if _file is None or isinstance(_file, FieldFile): + # FieldFile declares that no new file has been uploaded and we do not need to check on the file again + return super_valid + + mime_type_valid = self.document_model.is_mime_type_valid(_file) + if not mime_type_valid: + self.add_error( + "file", + FILE_TYPE_UNSUPPORTED + ) + + file_size_valid = self.document_model.is_file_size_valid(_file) + if not file_size_valid: + self.add_error( + "file", + FILE_SIZE_TOO_LARGE + ) + + file_valid = mime_type_valid and file_size_valid + return super_valid and file_valid + + def save(self): + with transaction.atomic(): + action = UserActionLogEntry.get_created_action(self.user) + edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document")) + + doc = self.document_model.objects.create( + created=action, + title=self.cleaned_data["title"], + comment=self.cleaned_data["comment"], + file=self.cleaned_data["file"], + date_of_creation=self.cleaned_data["creation_date"], + instance=self.instance, + ) + + self.instance.log.add(edited_action) + self.instance.modified = edited_action + self.instance.save() + + return doc + + +class EditDocumentModalForm(NewDocumentModalForm): + document = None + document_model = AbstractDocument + + def __init__(self, *args, **kwargs): + self.document = kwargs.pop("document", None) + super().__init__(*args, **kwargs) + self.form_title = _("Edit document") + form_data = { + "title": self.document.title, + "comment": self.document.comment, + "creation_date": str(self.document.date_of_creation), + "file": self.document.file, + } + self.load_initial_data(form_data) + + def save(self): + with transaction.atomic(): + document = self.document + file = self.cleaned_data.get("file", None) + + document.title = self.cleaned_data.get("title", None) + document.comment = self.cleaned_data.get("comment", None) + document.date_of_creation = self.cleaned_data.get("creation_date", None) + if not isinstance(file, FieldFile): + document.replace_file(file) + document.save() + + self.instance.mark_as_edited(self.user, self.request, edit_comment=DOCUMENT_EDITED) + + return document + diff --git a/konova/forms/modals/record_form.py b/konova/forms/modals/record_form.py new file mode 100644 index 00000000..812b697a --- /dev/null +++ b/konova/forms/modals/record_form.py @@ -0,0 +1,123 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 15.08.22 + +""" +from django import forms +from django.db import transaction +from django.utils.translation import gettext_lazy as _ + +from konova.forms.modals.base_form import BaseModalForm +from konova.models import RecordableObjectMixin + + +class RecordModalForm(BaseModalForm): + """ Modal form for recording data + + """ + confirm = forms.BooleanField( + label=_("Confirm record"), + label_suffix="", + widget=forms.CheckboxInput(), + required=True, + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Record data") + self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) + # Disable automatic w-100 setting for this type of modal form. Looks kinda strange + self.fields["confirm"].widget.attrs["class"] = "" + + if self.instance.recorded: + # unrecord! + self.fields["confirm"].label = _("Confirm unrecord") + self.form_title = _("Unrecord data") + self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name) + + if not isinstance(self.instance, RecordableObjectMixin): + raise NotImplementedError + + def is_valid(self): + """ Checks for instance's validity and data quality + + Returns: + + """ + from intervention.models import Intervention + super_val = super().is_valid() + if self.instance.recorded: + # If user wants to unrecord an already recorded dataset, we do not need to perform custom checks + return super_val + checker = self.instance.quality_check() + for msg in checker.messages: + self.add_error( + "confirm", + msg + ) + valid = checker.valid + # Special case: Intervention + # Add direct checks for related compensations + if isinstance(self.instance, Intervention): + comps_valid = self._are_compensations_valid() + valid = valid and comps_valid + return super_val and valid + + def _are_deductions_valid(self): + """ Performs validity checks on deductions and their eco-account + + Returns: + + """ + deductions = self.instance.deductions.all() + for deduction in deductions: + checker = deduction.account.quality_check() + for msg in checker.messages: + self.add_error( + "confirm", + f"{deduction.account.identifier}: {msg}" + ) + return checker.valid + return True + + def _are_compensations_valid(self): + """ Runs a special case for intervention-compensations validity + + Returns: + + """ + comps = self.instance.compensations.filter( + deleted=None, + ) + comps_valid = True + for comp in comps: + checker = comp.quality_check() + comps_valid = comps_valid and checker.valid + for msg in checker.messages: + self.add_error( + "confirm", + f"{comp.identifier}: {msg}" + ) + + deductions_valid = self._are_deductions_valid() + + return comps_valid and deductions_valid + + def save(self): + with transaction.atomic(): + if self.cleaned_data["confirm"]: + if self.instance.recorded: + self.instance.set_unrecorded(self.user) + else: + self.instance.set_recorded(self.user) + return self.instance + + def check_for_recorded_instance(self): + """ Overwrite the check method for doing nothing on the RecordModalForm + + Returns: + + """ + pass diff --git a/konova/forms/modals/remove_form.py b/konova/forms/modals/remove_form.py new file mode 100644 index 00000000..7a146268 --- /dev/null +++ b/konova/forms/modals/remove_form.py @@ -0,0 +1,58 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 15.08.22 + +""" +from django import forms +from django.utils.translation import gettext_lazy as _ + +from konova.forms.modals.base_form import BaseModalForm +from konova.models import BaseObject + + +class RemoveModalForm(BaseModalForm): + """ Generic removing modal form + + Can be used for anything, where removing shall be confirmed by the user a second time. + + """ + confirm = forms.BooleanField( + label=_("Confirm"), + label_suffix=_(""), + widget=forms.CheckboxInput(), + required=True, + ) + + def __init__(self, *args, **kwargs): + self.template = "modal/modal_form.html" + super().__init__(*args, **kwargs) + self.form_title = _("Remove") + self.form_caption = _("Are you sure?") + # Disable automatic w-100 setting for this type of modal form. Looks kinda strange + self.fields["confirm"].widget.attrs["class"] = "" + + def save(self): + if isinstance(self.instance, BaseObject): + self.instance.mark_as_deleted(self.user) + else: + # If the class does not provide restorable delete functionality, we must delete the entry finally + self.instance.delete() + + +class RemoveDeadlineModalForm(RemoveModalForm): + """ Removing modal form for deadlines + + Can be used for anything, where removing shall be confirmed by the user a second time. + + """ + deadline = None + + def __init__(self, *args, **kwargs): + deadline = kwargs.pop("deadline", None) + self.deadline = deadline + super().__init__(*args, **kwargs) + + def save(self): + self.instance.remove_deadline(self) \ No newline at end of file diff --git a/konova/forms/modals/resubmission_form.py b/konova/forms/modals/resubmission_form.py new file mode 100644 index 00000000..d1d846f6 --- /dev/null +++ b/konova/forms/modals/resubmission_form.py @@ -0,0 +1,85 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 15.08.22 + +""" +import datetime + +from django import forms +from django.core.exceptions import ObjectDoesNotExist +from django.db import transaction +from django.utils.translation import gettext_lazy as _ + +from konova.forms.modals.base_form import BaseModalForm +from konova.models import Resubmission + + +class ResubmissionModalForm(BaseModalForm): + date = forms.DateField( + label_suffix=_(""), + label=_("Date"), + help_text=_("When do you want to be reminded?"), + widget=forms.DateInput( + attrs={ + "type": "date", + "data-provide": "datepicker", + "class": "form-control", + }, + format="%d.%m.%Y" + ) + ) + comment = forms.CharField( + required=False, + label=_("Comment"), + label_suffix=_(""), + help_text=_("Additional comment"), + widget=forms.Textarea( + attrs={ + "cols": 30, + "rows": 5, + "class": "form-control", + } + ) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Resubmission") + self.form_caption = _("Set your resubmission for this entry.") + self.action_url = None + + try: + self.resubmission = self.instance.resubmissions.get( + user=self.user + ) + self.initialize_form_field("date", str(self.resubmission.resubmit_on)) + self.initialize_form_field("comment", self.resubmission.comment) + except ObjectDoesNotExist: + self.resubmission = Resubmission() + + def is_valid(self): + super_valid = super().is_valid() + self_valid = True + + date = self.cleaned_data.get("date") + today = datetime.date.today() + if date <= today: + self.add_error( + "date", + _("The date should be in the future") + ) + self_valid = False + + return super_valid and self_valid + + def save(self): + with transaction.atomic(): + self.resubmission.user = self.user + self.resubmission.resubmit_on = self.cleaned_data.get("date") + self.resubmission.comment = self.cleaned_data.get("comment") + self.resubmission.save() + self.instance.resubmissions.add(self.resubmission) + return self.resubmission + diff --git a/konova/forms/remove_form.py b/konova/forms/remove_form.py new file mode 100644 index 00000000..d5c884a6 --- /dev/null +++ b/konova/forms/remove_form.py @@ -0,0 +1,54 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 15.08.22 + +""" +from django import forms +from django.db import transaction +from django.utils.translation import gettext_lazy as _ + +from konova.forms.base_form import BaseForm +from user.models import UserActionLogEntry, User + + +class RemoveForm(BaseForm): + check = forms.BooleanField( + label=_("Confirm"), + label_suffix=_(""), + required=True, + ) + + def __init__(self, *args, **kwargs): + self.object_to_remove = kwargs.pop("object_to_remove", None) + self.remove_post_url = kwargs.pop("remove_post_url", "") + self.cancel_url = kwargs.pop("cancel_url", "") + + super().__init__(*args, **kwargs) + + self.form_title = _("Remove") + if self.object_to_remove is not None: + self.form_caption = _("You are about to remove {} {}").format(self.object_to_remove.__class__.__name__, self.object_to_remove) + self.action_url = self.remove_post_url + self.cancel_redirect = self.cancel_url + + def is_checked(self) -> bool: + return self.cleaned_data.get("check", False) + + def save(self, user: User): + """ Perform generic removing by running the form typical 'save()' method + + Args: + user (User): The performing user + + Returns: + + """ + if self.object_to_remove is not None and self.is_checked(): + with transaction.atomic(): + self.object_to_remove.is_active = False + action = UserActionLogEntry.get_deleted_action(user) + self.object_to_remove.deleted = action + self.object_to_remove.save() + return self.object_to_remove diff --git a/konova/migrations/0005_auto_20220216_0856.py b/konova/migrations/0005_auto_20220216_0856.py index 567e2065..8626b7c5 100644 --- a/konova/migrations/0005_auto_20220216_0856.py +++ b/konova/migrations/0005_auto_20220216_0856.py @@ -33,6 +33,7 @@ class Migration(migrations.Migration): dependencies = [ ('konova', '0004_auto_20220209_0839'), + ('compensation', '0002_auto_20220114_0936'), ] operations = [ diff --git a/konova/migrations/0014_resubmission.py b/konova/migrations/0014_resubmission.py new file mode 100644 index 00000000..f0ef9e7e --- /dev/null +++ b/konova/migrations/0014_resubmission.py @@ -0,0 +1,33 @@ +# Generated by Django 3.1.3 on 2022-08-15 06:03 + +from django.conf import settings +from django.db import migrations, models +import django.db.models.deletion +import uuid + + +class Migration(migrations.Migration): + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ('user', '0006_auto_20220815_0759'), + ('konova', '0013_auto_20220713_0814'), + ] + + operations = [ + migrations.CreateModel( + name='Resubmission', + fields=[ + ('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)), + ('resubmit_on', models.DateField(help_text='On which date the resubmission should be performed')), + ('resubmission_sent', models.BooleanField(default=False, help_text='Whether a resubmission has been sent or not')), + ('comment', models.TextField(blank=True, help_text='Optional comment for the user itself', null=True)), + ('created', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='user.useractionlogentry')), + ('modified', models.ForeignKey(blank=True, help_text='Last modified', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='user.useractionlogentry')), + ('user', models.ForeignKey(help_text='The user who wants to be notifed', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)), + ], + options={ + 'abstract': False, + }, + ), + ] diff --git a/konova/models/object.py b/konova/models/object.py index 0fbd6e8b..8af95e18 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -749,7 +749,6 @@ class GeoReferencedMixin(models.Model): class ResubmitableObjectMixin(models.Model): resubmissions = models.ManyToManyField( "konova.Resubmission", - null=True, blank=True, related_name="+", ) diff --git a/konova/utils/documents.py b/konova/utils/documents.py index f9b15160..3e8f6f12 100644 --- a/konova/utils/documents.py +++ b/konova/utils/documents.py @@ -5,10 +5,9 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 01.09.21 """ -from django.http import FileResponse, HttpRequest, HttpResponse, Http404 -from django.utils.translation import gettext_lazy as _ +from django.http import FileResponse, HttpRequest, Http404 -from konova.forms import RemoveModalForm +from konova.forms.modals import RemoveModalForm from konova.models import AbstractDocument from konova.utils.message_templates import DOCUMENT_REMOVED_TEMPLATE diff --git a/user/forms.py b/user/forms.py index 12688b46..0649eaf3 100644 --- a/user/forms.py +++ b/user/forms.py @@ -15,7 +15,8 @@ from api.models import APIUserToken from intervention.inputs import GenerateInput from user.models import User, UserNotification, Team -from konova.forms import BaseForm, BaseModalForm, RemoveModalForm +from konova.forms.modals import BaseModalForm, RemoveModalForm +from konova.forms import BaseForm class UserNotificationForm(BaseForm): diff --git a/user/migrations/0006_auto_20220815_0759.py b/user/migrations/0006_auto_20220815_0759.py new file mode 100644 index 00000000..53861083 --- /dev/null +++ b/user/migrations/0006_auto_20220815_0759.py @@ -0,0 +1,18 @@ +# Generated by Django 3.1.3 on 2022-08-15 05:59 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('user', '0005_team_deleted'), + ] + + operations = [ + migrations.AlterField( + model_name='usernotification', + name='id', + field=models.CharField(choices=[('NOTIFY_ON_SHARED_ACCESS_REMOVED', 'NOTIFY_ON_SHARED_ACCESS_REMOVED'), ('NOTIFY_ON_SHARED_DATA_RECORDED', 'NOTIFY_ON_SHARED_DATA_RECORDED'), ('NOTIFY_ON_SHARED_DATA_DELETED', 'NOTIFY_ON_SHARED_DATA_DELETED'), ('NOTIFY_ON_SHARED_DATA_CHECKED', 'NOTIFY_ON_SHARED_DATA_CHECKED'), ('NOTIFY_ON_SHARED_ACCESS_GAINED', 'NOTIFY_ON_SHARED_ACCESS_GAINED'), ('NOTIFY_ON_DEDUCTION_CHANGES', 'NOTIFY_ON_DEDUCTION_CHANGES')], max_length=500, primary_key=True, serialize=False), + ), + ] From 1367fd2b5f7759980f920932154d1c59513a15d4 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 18 Aug 2022 09:54:49 +0200 Subject: [PATCH 11/48] Compensation forms refactoring * splits compensation/forms.py and /modalForms.py into individual files inside new packages * general forms stay in new files in compensation/forms * modal forms stay in new files in compensation/forms/modals --- compensation/forms/__init__.py | 7 + compensation/forms/compensation.py | 238 ++++++++ compensation/forms/eco_account.py | 212 +++++++ compensation/forms/forms.py | 539 ------------------ compensation/forms/mixins.py | 117 ++++ compensation/forms/modalForms.py | 534 ----------------- compensation/forms/modals/__init__.py | 7 + .../forms/modals/compensation_action.py | 155 +++++ compensation/forms/modals/deadline.py | 92 +++ compensation/forms/modals/document.py | 17 + compensation/forms/modals/payment.py | 136 +++++ compensation/forms/modals/state.py | 179 ++++++ compensation/views/compensation.py | 16 +- compensation/views/eco_account.py | 15 +- compensation/views/payment.py | 3 +- ema/forms.py | 4 +- ema/views.py | 12 +- 17 files changed, 1188 insertions(+), 1095 deletions(-) create mode 100644 compensation/forms/__init__.py create mode 100644 compensation/forms/compensation.py create mode 100644 compensation/forms/eco_account.py delete mode 100644 compensation/forms/forms.py create mode 100644 compensation/forms/mixins.py delete mode 100644 compensation/forms/modalForms.py create mode 100644 compensation/forms/modals/__init__.py create mode 100644 compensation/forms/modals/compensation_action.py create mode 100644 compensation/forms/modals/deadline.py create mode 100644 compensation/forms/modals/document.py create mode 100644 compensation/forms/modals/payment.py create mode 100644 compensation/forms/modals/state.py diff --git a/compensation/forms/__init__.py b/compensation/forms/__init__.py new file mode 100644 index 00000000..ca978536 --- /dev/null +++ b/compensation/forms/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" diff --git a/compensation/forms/compensation.py b/compensation/forms/compensation.py new file mode 100644 index 00000000..1672e161 --- /dev/null +++ b/compensation/forms/compensation.py @@ -0,0 +1,238 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from dal import autocomplete +from django import forms +from django.db import transaction +from django.urls import reverse, reverse_lazy +from django.utils.translation import gettext_lazy as _ + +from compensation.forms.mixins import CEFCompensationFormMixin, CoherenceCompensationFormMixin, PikCompensationFormMixin +from compensation.models import Compensation +from intervention.inputs import GenerateInput +from intervention.models import Intervention +from konova.forms import BaseForm, SimpleGeomForm +from konova.utils.message_templates import COMPENSATION_ADDED_TEMPLATE, EDITED_GENERAL_DATA +from user.models import UserActionLogEntry, User + + +class AbstractCompensationForm(BaseForm): + """ Abstract form for compensations + + Holds all important form fields, which are used in compensation and eco account forms + + """ + identifier = forms.CharField( + label=_("Identifier"), + label_suffix="", + max_length=255, + help_text=_("Generated automatically"), + widget=GenerateInput( + attrs={ + "class": "form-control", + "url": None, # Needs to be set in inheriting constructors + } + ) + ) + title = forms.CharField( + label=_("Title"), + label_suffix="", + help_text=_("An explanatory name"), + max_length=255, + widget=forms.TextInput( + attrs={ + "placeholder": _("Compensation XY; Location ABC"), + "class": "form-control", + } + ) + ) + comment = forms.CharField( + label_suffix="", + label=_("Comment"), + required=False, + help_text=_("Additional comment"), + widget=forms.Textarea( + attrs={ + "rows": 5, + "class": "form-control" + } + ) + ) + + class Meta: + abstract = True + + +class NewCompensationForm(AbstractCompensationForm, + CEFCompensationFormMixin, + CoherenceCompensationFormMixin, + PikCompensationFormMixin): + """ Form for creating new compensations. + + Can be initialized with an intervention id for preselecting the related intervention. + form = NewCompensationForm(request.POST or None, intervention_id=intervention_id) + ... + The intervention id will not be resolved into the intervention ORM object but instead will be used to initialize + the related form field. + + """ + intervention = forms.ModelChoiceField( + label=_("compensates intervention"), + label_suffix="", + help_text=_("Select the intervention for which this compensation compensates"), + queryset=Intervention.objects.filter( + deleted=None, + ), + widget=autocomplete.ModelSelect2( + url="interventions-autocomplete", + attrs={ + "data-placeholder": _("Click for selection"), + "data-minimum-input-length": 3, + } + ), + ) + + # Define a field order for a nicer layout instead of running with the inheritance result + field_order = [ + "identifier", + "title", + "intervention", + "is_pik", + "is_cef", + "is_coherence_keeping", + "comment", + ] + + def __init__(self, *args, **kwargs): + intervention_id = kwargs.pop("intervention_id", None) + super().__init__(*args, **kwargs) + self.form_title = _("New compensation") + + # If the compensation shall directly be initialized from an intervention, we need to fill in the intervention id + # and disable the form field. + # Furthermore the action_url needs to be set accordingly. + if intervention_id is not None: + self.initialize_form_field("intervention", intervention_id) + self.disable_form_field("intervention") + self.action_url = reverse("compensation:new", args=(intervention_id,)) + self.cancel_redirect = reverse("intervention:detail", args=(intervention_id,)) + else: + self.action_url = reverse("compensation:new") + self.cancel_redirect = reverse("compensation:index") + + tmp = Compensation() + identifier = tmp.generate_new_identifier() + self.initialize_form_field("identifier", identifier) + self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id") + + def __create_comp(self, user, geom_form) -> Compensation: + """ Creates the compensation from form data + + Args: + user (User): The performing user + geom_form (SimpleGeomForm): The geometry form + + Returns: + comp (Compensation): The compensation object + """ + # Fetch data from cleaned POST values + identifier = self.cleaned_data.get("identifier", None) + title = self.cleaned_data.get("title", None) + intervention = self.cleaned_data.get("intervention", None) + is_cef = self.cleaned_data.get("is_cef", None) + is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None) + is_pik = self.cleaned_data.get("is_pik", None) + comment = self.cleaned_data.get("comment", None) + + # Create log entry + action = UserActionLogEntry.get_created_action(user) + # Process the geometry form + geometry = geom_form.save(action) + + # Finally create main object + comp = Compensation.objects.create( + identifier=identifier, + title=title, + intervention=intervention, + created=action, + is_cef=is_cef, + is_coherence_keeping=is_coherence_keeping, + is_pik=is_pik, + geometry=geometry, + comment=comment, + ) + + # Add the log entry to the main objects log list + comp.log.add(action) + return comp + + def save(self, user: User, geom_form: SimpleGeomForm): + with transaction.atomic(): + comp = self.__create_comp(user, geom_form) + comp.intervention.mark_as_edited(user, edit_comment=COMPENSATION_ADDED_TEMPLATE.format(comp.identifier)) + return comp + + +class EditCompensationForm(NewCompensationForm): + """ Form for editing compensations + + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Edit compensation") + self.action_url = reverse("compensation:edit", args=(self.instance.id,)) + self.cancel_redirect = reverse("compensation:detail", args=(self.instance.id,)) + + # Initialize form data + form_data = { + "identifier": self.instance.identifier, + "title": self.instance.title, + "intervention": self.instance.intervention, + "is_cef": self.instance.is_cef, + "is_coherence_keeping": self.instance.is_coherence_keeping, + "is_pik": self.instance.is_pik, + "comment": self.instance.comment, + } + disabled_fields = [] + self.load_initial_data( + form_data, + disabled_fields + ) + + def save(self, user: User, geom_form: SimpleGeomForm): + with transaction.atomic(): + # Fetch data from cleaned POST values + identifier = self.cleaned_data.get("identifier", None) + title = self.cleaned_data.get("title", None) + intervention = self.cleaned_data.get("intervention", None) + is_cef = self.cleaned_data.get("is_cef", None) + is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None) + is_pik = self.cleaned_data.get("is_pik", None) + comment = self.cleaned_data.get("comment", None) + + # Create log entry + action = UserActionLogEntry.get_edited_action(user) + + # Process the geometry form + geometry = geom_form.save(action) + + # Finally create main object + self.instance.identifier = identifier + self.instance.title = title + self.instance.intervention = intervention + self.instance.geometry = geometry + self.instance.is_cef = is_cef + self.instance.is_coherence_keeping = is_coherence_keeping + self.instance.comment = comment + self.instance.is_pik = is_pik + self.instance.modified = action + self.instance.save() + + self.instance.log.add(action) + + intervention.mark_as_edited(user, self.request, EDITED_GENERAL_DATA) + return self.instance \ No newline at end of file diff --git a/compensation/forms/eco_account.py b/compensation/forms/eco_account.py new file mode 100644 index 00000000..6bc12618 --- /dev/null +++ b/compensation/forms/eco_account.py @@ -0,0 +1,212 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.db import transaction +from django.urls import reverse, reverse_lazy +from django.utils.translation import gettext_lazy as _ +from compensation.forms.compensation import AbstractCompensationForm +from compensation.forms.mixins import CompensationResponsibleFormMixin, PikCompensationFormMixin +from compensation.models import EcoAccount +from intervention.models import Handler, Responsibility, Legal +from konova.forms import SimpleGeomForm +from user.models import User, UserActionLogEntry + + +class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin): + """ Form for creating eco accounts + + Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin + + """ + surface = forms.DecimalField( + min_value=0.00, + decimal_places=2, + label=_("Available Surface"), + label_suffix="", + required=False, + help_text=_("The amount that can be used for deductions"), + widget=forms.NumberInput( + attrs={ + "class": "form-control", + "placeholder": "0,00" + } + ) + ) + registration_date = forms.DateField( + label=_("Agreement date"), + label_suffix="", + help_text=_("When did the parties agree on this?"), + required=False, + widget=forms.DateInput( + attrs={ + "type": "date", + "class": "form-control", + }, + format="%d.%m.%Y" + ) + ) + + field_order = [ + "identifier", + "title", + "conservation_office", + "registration_date", + "surface", + "conservation_file_number", + "is_pik", + "handler_type", + "handler_detail", + "comment", + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("New Eco-Account") + + self.action_url = reverse("compensation:acc:new") + self.cancel_redirect = reverse("compensation:acc:index") + + tmp = EcoAccount() + identifier = tmp.generate_new_identifier() + self.initialize_form_field("identifier", identifier) + self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:acc:new-id") + self.fields["title"].widget.attrs["placeholder"] = _("Eco-Account XY; Location ABC") + + def save(self, user: User, geom_form: SimpleGeomForm): + with transaction.atomic(): + # Fetch data from cleaned POST values + identifier = self.cleaned_data.get("identifier", None) + title = self.cleaned_data.get("title", None) + registration_date = self.cleaned_data.get("registration_date", None) + handler_type = self.cleaned_data.get("handler_type", None) + handler_detail = self.cleaned_data.get("handler_detail", None) + surface = self.cleaned_data.get("surface", None) + conservation_office = self.cleaned_data.get("conservation_office", None) + conservation_file_number = self.cleaned_data.get("conservation_file_number", None) + is_pik = self.cleaned_data.get("is_pik", None) + comment = self.cleaned_data.get("comment", None) + + # Create log entry + action = UserActionLogEntry.get_created_action(user) + # Process the geometry form + geometry = geom_form.save(action) + + handler = Handler.objects.create( + type=handler_type, + detail=handler_detail, + ) + + responsible = Responsibility.objects.create( + handler=handler, + conservation_file_number=conservation_file_number, + conservation_office=conservation_office, + ) + + legal = Legal.objects.create( + registration_date=registration_date + ) + + # Finally create main object + acc = EcoAccount.objects.create( + identifier=identifier, + title=title, + responsible=responsible, + deductable_surface=surface, + created=action, + geometry=geometry, + comment=comment, + is_pik=is_pik, + legal=legal + ) + acc.share_with_user(user) + + # Add the log entry to the main objects log list + acc.log.add(action) + return acc + + +class EditEcoAccountForm(NewEcoAccountForm): + """ Form for editing eco accounts + + """ + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Edit Eco-Account") + + self.action_url = reverse("compensation:acc:edit", args=(self.instance.id,)) + self.cancel_redirect = reverse("compensation:acc:detail", args=(self.instance.id,)) + + # Initialize form data + reg_date = self.instance.legal.registration_date + if reg_date is not None: + reg_date = reg_date.isoformat() + + form_data = { + "identifier": self.instance.identifier, + "title": self.instance.title, + "surface": self.instance.deductable_surface, + "handler_type": self.instance.responsible.handler.type, + "handler_detail": self.instance.responsible.handler.detail, + "registration_date": reg_date, + "conservation_office": self.instance.responsible.conservation_office, + "conservation_file_number": self.instance.responsible.conservation_file_number, + "is_pik": self.instance.is_pik, + "comment": self.instance.comment, + } + disabled_fields = [] + self.load_initial_data( + form_data, + disabled_fields + ) + + def save(self, user: User, geom_form: SimpleGeomForm): + with transaction.atomic(): + # Fetch data from cleaned POST values + identifier = self.cleaned_data.get("identifier", None) + title = self.cleaned_data.get("title", None) + registration_date = self.cleaned_data.get("registration_date", None) + handler_type = self.cleaned_data.get("handler_type", None) + handler_detail = self.cleaned_data.get("handler_detail", None) + surface = self.cleaned_data.get("surface", None) + conservation_office = self.cleaned_data.get("conservation_office", None) + conservation_file_number = self.cleaned_data.get("conservation_file_number", None) + comment = self.cleaned_data.get("comment", None) + is_pik = self.cleaned_data.get("is_pik", None) + + # Create log entry + action = UserActionLogEntry.get_edited_action(user) + + # Process the geometry form + geometry = geom_form.save(action) + + # Update responsible data + self.instance.responsible.handler.type = handler_type + self.instance.responsible.handler.detail = handler_detail + self.instance.responsible.handler.save() + self.instance.responsible.conservation_office = conservation_office + self.instance.responsible.conservation_file_number = conservation_file_number + self.instance.responsible.save() + + # Update legal data + self.instance.legal.registration_date = registration_date + self.instance.legal.save() + + # Update main oject data + self.instance.identifier = identifier + self.instance.title = title + self.instance.deductable_surface = surface + self.instance.geometry = geometry + self.instance.comment = comment + self.instance.is_pik = is_pik + self.instance.modified = action + self.instance.save() + + # Add the log entry to the main objects log list + self.instance.log.add(action) + return self.instance diff --git a/compensation/forms/forms.py b/compensation/forms/forms.py deleted file mode 100644 index 8f28c5ff..00000000 --- a/compensation/forms/forms.py +++ /dev/null @@ -1,539 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 04.12.20 - -""" -from dal import autocomplete -from user.models import User -from django.db import transaction -from django.urls import reverse_lazy, reverse -from django.utils.translation import gettext_lazy as _ -from django import forms - -from codelist.models import KonovaCode -from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID -from compensation.models import Compensation, EcoAccount -from intervention.inputs import GenerateInput -from intervention.models import Intervention, Responsibility, Legal, Handler -from konova.forms import BaseForm, SimpleGeomForm -from konova.utils.message_templates import EDITED_GENERAL_DATA, COMPENSATION_ADDED_TEMPLATE -from user.models import UserActionLogEntry - - -class AbstractCompensationForm(BaseForm): - """ Abstract form for compensations - - Holds all important form fields, which are used in compensation and eco account forms - - """ - identifier = forms.CharField( - label=_("Identifier"), - label_suffix="", - max_length=255, - help_text=_("Generated automatically"), - widget=GenerateInput( - attrs={ - "class": "form-control", - "url": None, # Needs to be set in inheriting constructors - } - ) - ) - title = forms.CharField( - label=_("Title"), - label_suffix="", - help_text=_("An explanatory name"), - max_length=255, - widget=forms.TextInput( - attrs={ - "placeholder": _("Compensation XY; Location ABC"), - "class": "form-control", - } - ) - ) - comment = forms.CharField( - label_suffix="", - label=_("Comment"), - required=False, - help_text=_("Additional comment"), - widget=forms.Textarea( - attrs={ - "rows": 5, - "class": "form-control" - } - ) - ) - - class Meta: - abstract = True - - -class CompensationResponsibleFormMixin(forms.Form): - """ Encapsulates form fields used in different compensation related models like EcoAccount or EMA - - """ - conservation_office = forms.ModelChoiceField( - label=_("Conservation office"), - label_suffix="", - help_text=_("Select the responsible office"), - queryset=KonovaCode.objects.filter( - is_archived=False, - is_leaf=True, - code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID], - ), - widget=autocomplete.ModelSelect2( - url="codes-conservation-office-autocomplete", - attrs={ - "data-placeholder": _("Click for selection") - } - ), - ) - conservation_file_number = forms.CharField( - label=_("Conservation office file number"), - label_suffix="", - max_length=255, - required=False, - widget=forms.TextInput( - attrs={ - "placeholder": _("ETS-123/ABC.456"), - "class": "form-control", - } - ) - ) - - handler_type = forms.ModelChoiceField( - label=_("Eco-Account handler type"), - label_suffix="", - help_text=_("What type of handler is responsible for the ecoaccount?"), - required=False, - queryset=KonovaCode.objects.filter( - is_archived=False, - is_leaf=True, - code_lists__in=[CODELIST_HANDLER_ID], - ), - widget=autocomplete.ModelSelect2( - url="codes-handler-autocomplete", - attrs={ - "data-placeholder": _("Click for selection"), - } - ), - ) - handler_detail = forms.CharField( - label=_("Eco-Account handler detail"), - label_suffix="", - max_length=255, - required=False, - help_text=_("Detail input on the handler"), - widget=forms.TextInput( - attrs={ - "placeholder": _("Company Mustermann"), - "class": "form-control", - } - ) - ) - - -class CEFCompensationFormMixin(forms.Form): - """ A form mixin, providing CEF compensation field - - """ - is_cef = forms.BooleanField( - label_suffix="", - label=_("Is CEF"), - help_text=_("Optionally: Whether this compensation is a CEF compensation?"), - required=False, - widget=forms.CheckboxInput() - ) - - -class CoherenceCompensationFormMixin(forms.Form): - """ A form mixin, providing coherence compensation field - - """ - is_coherence_keeping = forms.BooleanField( - label_suffix="", - label=_("Is coherence keeping"), - help_text=_("Optionally: Whether this compensation is a coherence keeping compensation?"), - required=False, - widget=forms.CheckboxInput() - ) - - -class PikCompensationFormMixin(forms.Form): - """ A form mixin, providing PIK compensation field - - """ - is_pik = forms.BooleanField( - label_suffix="", - label=_("Is PIK"), - help_text=_("Optionally: Whether this compensation is a compensation integrated in production?"), - required=False, - widget=forms.CheckboxInput() - ) - - -class NewCompensationForm(AbstractCompensationForm, - CEFCompensationFormMixin, - CoherenceCompensationFormMixin, - PikCompensationFormMixin): - """ Form for creating new compensations. - - Can be initialized with an intervention id for preselecting the related intervention. - form = NewCompensationForm(request.POST or None, intervention_id=intervention_id) - ... - The intervention id will not be resolved into the intervention ORM object but instead will be used to initialize - the related form field. - - """ - intervention = forms.ModelChoiceField( - label=_("compensates intervention"), - label_suffix="", - help_text=_("Select the intervention for which this compensation compensates"), - queryset=Intervention.objects.filter( - deleted=None, - ), - widget=autocomplete.ModelSelect2( - url="interventions-autocomplete", - attrs={ - "data-placeholder": _("Click for selection"), - "data-minimum-input-length": 3, - } - ), - ) - - # Define a field order for a nicer layout instead of running with the inheritance result - field_order = [ - "identifier", - "title", - "intervention", - "is_pik", - "is_cef", - "is_coherence_keeping", - "comment", - ] - - def __init__(self, *args, **kwargs): - intervention_id = kwargs.pop("intervention_id", None) - super().__init__(*args, **kwargs) - self.form_title = _("New compensation") - - # If the compensation shall directly be initialized from an intervention, we need to fill in the intervention id - # and disable the form field. - # Furthermore the action_url needs to be set accordingly. - if intervention_id is not None: - self.initialize_form_field("intervention", intervention_id) - self.disable_form_field("intervention") - self.action_url = reverse("compensation:new", args=(intervention_id,)) - self.cancel_redirect = reverse("intervention:detail", args=(intervention_id,)) - else: - self.action_url = reverse("compensation:new") - self.cancel_redirect = reverse("compensation:index") - - tmp = Compensation() - identifier = tmp.generate_new_identifier() - self.initialize_form_field("identifier", identifier) - self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id") - - def __create_comp(self, user, geom_form) -> Compensation: - """ Creates the compensation from form data - - Args: - user (User): The performing user - geom_form (SimpleGeomForm): The geometry form - - Returns: - comp (Compensation): The compensation object - """ - # Fetch data from cleaned POST values - identifier = self.cleaned_data.get("identifier", None) - title = self.cleaned_data.get("title", None) - intervention = self.cleaned_data.get("intervention", None) - is_cef = self.cleaned_data.get("is_cef", None) - is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None) - is_pik = self.cleaned_data.get("is_pik", None) - comment = self.cleaned_data.get("comment", None) - - # Create log entry - action = UserActionLogEntry.get_created_action(user) - # Process the geometry form - geometry = geom_form.save(action) - - # Finally create main object - comp = Compensation.objects.create( - identifier=identifier, - title=title, - intervention=intervention, - created=action, - is_cef=is_cef, - is_coherence_keeping=is_coherence_keeping, - is_pik=is_pik, - geometry=geometry, - comment=comment, - ) - - # Add the log entry to the main objects log list - comp.log.add(action) - return comp - - def save(self, user: User, geom_form: SimpleGeomForm): - with transaction.atomic(): - comp = self.__create_comp(user, geom_form) - comp.intervention.mark_as_edited(user, edit_comment=COMPENSATION_ADDED_TEMPLATE.format(comp.identifier)) - return comp - - -class EditCompensationForm(NewCompensationForm): - """ Form for editing compensations - - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Edit compensation") - self.action_url = reverse("compensation:edit", args=(self.instance.id,)) - self.cancel_redirect = reverse("compensation:detail", args=(self.instance.id,)) - - # Initialize form data - form_data = { - "identifier": self.instance.identifier, - "title": self.instance.title, - "intervention": self.instance.intervention, - "is_cef": self.instance.is_cef, - "is_coherence_keeping": self.instance.is_coherence_keeping, - "is_pik": self.instance.is_pik, - "comment": self.instance.comment, - } - disabled_fields = [] - self.load_initial_data( - form_data, - disabled_fields - ) - - def save(self, user: User, geom_form: SimpleGeomForm): - with transaction.atomic(): - # Fetch data from cleaned POST values - identifier = self.cleaned_data.get("identifier", None) - title = self.cleaned_data.get("title", None) - intervention = self.cleaned_data.get("intervention", None) - is_cef = self.cleaned_data.get("is_cef", None) - is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None) - is_pik = self.cleaned_data.get("is_pik", None) - comment = self.cleaned_data.get("comment", None) - - # Create log entry - action = UserActionLogEntry.get_edited_action(user) - - # Process the geometry form - geometry = geom_form.save(action) - - # Finally create main object - self.instance.identifier = identifier - self.instance.title = title - self.instance.intervention = intervention - self.instance.geometry = geometry - self.instance.is_cef = is_cef - self.instance.is_coherence_keeping = is_coherence_keeping - self.instance.comment = comment - self.instance.is_pik = is_pik - self.instance.modified = action - self.instance.save() - - self.instance.log.add(action) - - intervention.mark_as_edited(user, self.request, EDITED_GENERAL_DATA) - return self.instance - - -class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin): - """ Form for creating eco accounts - - Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin - - """ - surface = forms.DecimalField( - min_value=0.00, - decimal_places=2, - label=_("Available Surface"), - label_suffix="", - required=False, - help_text=_("The amount that can be used for deductions"), - widget=forms.NumberInput( - attrs={ - "class": "form-control", - "placeholder": "0,00" - } - ) - ) - registration_date = forms.DateField( - label=_("Agreement date"), - label_suffix="", - help_text=_("When did the parties agree on this?"), - required=False, - widget=forms.DateInput( - attrs={ - "type": "date", - "class": "form-control", - }, - format="%d.%m.%Y" - ) - ) - - field_order = [ - "identifier", - "title", - "conservation_office", - "registration_date", - "surface", - "conservation_file_number", - "is_pik", - "handler_type", - "handler_detail", - "comment", - ] - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("New Eco-Account") - - self.action_url = reverse("compensation:acc:new") - self.cancel_redirect = reverse("compensation:acc:index") - - tmp = EcoAccount() - identifier = tmp.generate_new_identifier() - self.initialize_form_field("identifier", identifier) - self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:acc:new-id") - self.fields["title"].widget.attrs["placeholder"] = _("Eco-Account XY; Location ABC") - - def save(self, user: User, geom_form: SimpleGeomForm): - with transaction.atomic(): - # Fetch data from cleaned POST values - identifier = self.cleaned_data.get("identifier", None) - title = self.cleaned_data.get("title", None) - registration_date = self.cleaned_data.get("registration_date", None) - handler_type = self.cleaned_data.get("handler_type", None) - handler_detail = self.cleaned_data.get("handler_detail", None) - surface = self.cleaned_data.get("surface", None) - conservation_office = self.cleaned_data.get("conservation_office", None) - conservation_file_number = self.cleaned_data.get("conservation_file_number", None) - is_pik = self.cleaned_data.get("is_pik", None) - comment = self.cleaned_data.get("comment", None) - - # Create log entry - action = UserActionLogEntry.get_created_action(user) - # Process the geometry form - geometry = geom_form.save(action) - - handler = Handler.objects.create( - type=handler_type, - detail=handler_detail, - ) - - responsible = Responsibility.objects.create( - handler=handler, - conservation_file_number=conservation_file_number, - conservation_office=conservation_office, - ) - - legal = Legal.objects.create( - registration_date=registration_date - ) - - # Finally create main object - acc = EcoAccount.objects.create( - identifier=identifier, - title=title, - responsible=responsible, - deductable_surface=surface, - created=action, - geometry=geometry, - comment=comment, - is_pik=is_pik, - legal=legal - ) - acc.share_with_user(user) - - # Add the log entry to the main objects log list - acc.log.add(action) - return acc - - -class EditEcoAccountForm(NewEcoAccountForm): - """ Form for editing eco accounts - - """ - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Edit Eco-Account") - - self.action_url = reverse("compensation:acc:edit", args=(self.instance.id,)) - self.cancel_redirect = reverse("compensation:acc:detail", args=(self.instance.id,)) - - # Initialize form data - reg_date = self.instance.legal.registration_date - if reg_date is not None: - reg_date = reg_date.isoformat() - - form_data = { - "identifier": self.instance.identifier, - "title": self.instance.title, - "surface": self.instance.deductable_surface, - "handler_type": self.instance.responsible.handler.type, - "handler_detail": self.instance.responsible.handler.detail, - "registration_date": reg_date, - "conservation_office": self.instance.responsible.conservation_office, - "conservation_file_number": self.instance.responsible.conservation_file_number, - "is_pik": self.instance.is_pik, - "comment": self.instance.comment, - } - disabled_fields = [] - self.load_initial_data( - form_data, - disabled_fields - ) - - def save(self, user: User, geom_form: SimpleGeomForm): - with transaction.atomic(): - # Fetch data from cleaned POST values - identifier = self.cleaned_data.get("identifier", None) - title = self.cleaned_data.get("title", None) - registration_date = self.cleaned_data.get("registration_date", None) - handler_type = self.cleaned_data.get("handler_type", None) - handler_detail = self.cleaned_data.get("handler_detail", None) - surface = self.cleaned_data.get("surface", None) - conservation_office = self.cleaned_data.get("conservation_office", None) - conservation_file_number = self.cleaned_data.get("conservation_file_number", None) - comment = self.cleaned_data.get("comment", None) - is_pik = self.cleaned_data.get("is_pik", None) - - # Create log entry - action = UserActionLogEntry.get_edited_action(user) - - # Process the geometry form - geometry = geom_form.save(action) - - # Update responsible data - self.instance.responsible.handler.type = handler_type - self.instance.responsible.handler.detail = handler_detail - self.instance.responsible.handler.save() - self.instance.responsible.conservation_office = conservation_office - self.instance.responsible.conservation_file_number = conservation_file_number - self.instance.responsible.save() - - # Update legal data - self.instance.legal.registration_date = registration_date - self.instance.legal.save() - - # Update main oject data - self.instance.identifier = identifier - self.instance.title = title - self.instance.deductable_surface = surface - self.instance.geometry = geometry - self.instance.comment = comment - self.instance.is_pik = is_pik - self.instance.modified = action - self.instance.save() - - # Add the log entry to the main objects log list - self.instance.log.add(action) - return self.instance diff --git a/compensation/forms/mixins.py b/compensation/forms/mixins.py new file mode 100644 index 00000000..db174808 --- /dev/null +++ b/compensation/forms/mixins.py @@ -0,0 +1,117 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from dal import autocomplete +from django import forms +from django.utils.translation import gettext_lazy as _ + +from codelist.models import KonovaCode +from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID + + +class CompensationResponsibleFormMixin(forms.Form): + """ Encapsulates form fields used in different compensation related models like EcoAccount or EMA + + """ + conservation_office = forms.ModelChoiceField( + label=_("Conservation office"), + label_suffix="", + help_text=_("Select the responsible office"), + queryset=KonovaCode.objects.filter( + is_archived=False, + is_leaf=True, + code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID], + ), + widget=autocomplete.ModelSelect2( + url="codes-conservation-office-autocomplete", + attrs={ + "data-placeholder": _("Click for selection") + } + ), + ) + conservation_file_number = forms.CharField( + label=_("Conservation office file number"), + label_suffix="", + max_length=255, + required=False, + widget=forms.TextInput( + attrs={ + "placeholder": _("ETS-123/ABC.456"), + "class": "form-control", + } + ) + ) + + handler_type = forms.ModelChoiceField( + label=_("Eco-Account handler type"), + label_suffix="", + help_text=_("What type of handler is responsible for the ecoaccount?"), + required=False, + queryset=KonovaCode.objects.filter( + is_archived=False, + is_leaf=True, + code_lists__in=[CODELIST_HANDLER_ID], + ), + widget=autocomplete.ModelSelect2( + url="codes-handler-autocomplete", + attrs={ + "data-placeholder": _("Click for selection"), + } + ), + ) + handler_detail = forms.CharField( + label=_("Eco-Account handler detail"), + label_suffix="", + max_length=255, + required=False, + help_text=_("Detail input on the handler"), + widget=forms.TextInput( + attrs={ + "placeholder": _("Company Mustermann"), + "class": "form-control", + } + ) + ) + + +class CEFCompensationFormMixin(forms.Form): + """ A form mixin, providing CEF compensation field + + """ + is_cef = forms.BooleanField( + label_suffix="", + label=_("Is CEF"), + help_text=_("Optionally: Whether this compensation is a CEF compensation?"), + required=False, + widget=forms.CheckboxInput() + ) + + +class CoherenceCompensationFormMixin(forms.Form): + """ A form mixin, providing coherence compensation field + + """ + is_coherence_keeping = forms.BooleanField( + label_suffix="", + label=_("Is coherence keeping"), + help_text=_("Optionally: Whether this compensation is a coherence keeping compensation?"), + required=False, + widget=forms.CheckboxInput() + ) + + +class PikCompensationFormMixin(forms.Form): + """ A form mixin, providing PIK compensation field + + """ + is_pik = forms.BooleanField( + label_suffix="", + label=_("Is PIK"), + help_text=_("Optionally: Whether this compensation is a compensation integrated in production?"), + required=False, + widget=forms.CheckboxInput() + ) \ No newline at end of file diff --git a/compensation/forms/modalForms.py b/compensation/forms/modalForms.py deleted file mode 100644 index ef21aa3f..00000000 --- a/compensation/forms/modalForms.py +++ /dev/null @@ -1,534 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 04.10.21 - -""" -from bootstrap_modal_forms.utils import is_ajax -from dal import autocomplete -from django import forms -from django.contrib import messages -from django.http import HttpRequest, HttpResponseRedirect -from django.shortcuts import render -from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _ - -from codelist.models import KonovaCode -from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \ - CODELIST_COMPENSATION_ACTION_DETAIL_ID -from compensation.models import CompensationDocument, EcoAccountDocument -from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \ - CompensationStateTreeRadioSelect -from konova.contexts import BaseContext -from konova.forms.modals import BaseModalForm, NewDocumentModalForm, RemoveModalForm -from konova.models import DeadlineType -from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, \ - ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED - - -class NewPaymentForm(BaseModalForm): - """ Form handling payment related input - - """ - amount = forms.DecimalField( - min_value=0.00, - decimal_places=2, - label=_con("money", "Amount"), # contextual translation - label_suffix=_(""), - help_text=_("in Euro"), - widget=forms.NumberInput( - attrs={ - "class": "form-control", - "placeholder": "0,00", - } - ) - ) - due = forms.DateField( - label=_("Due on"), - label_suffix=_(""), - required=False, - help_text=_("Due on which date"), - widget=forms.DateInput( - attrs={ - "type": "date", - "data-provide": "datepicker", - "class": "form-control", - }, - format="%d.%m.%Y" - ) - ) - comment = forms.CharField( - max_length=200, - required=False, - label=_("Comment"), - label_suffix=_(""), - help_text=_("Additional comment, maximum {} letters").format(200), - widget=forms.Textarea( - attrs={ - "rows": 5, - "class": "form-control" - } - ) - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.intervention = self.instance - self.form_title = _("Payment") - self.form_caption = _("Add a payment for intervention '{}'").format(self.intervention.title) - - def is_valid(self): - """ - Checks on form validity. - - For this form we need to make sure that a date or a comment is set. - If both are missing, the user needs to enter at least an explanation why - there is no date to be entered. - - Returns: - is_valid (bool): True if valid, False otherwise - """ - super_valid = super().is_valid() - date = self.cleaned_data["due"] - comment = self.cleaned_data["comment"] or None - if not date and not comment: - # At least one needs to be set! - self.add_error( - "comment", - _("If there is no date you can enter, please explain why.") - ) - return False - return super_valid - - def save(self): - pay = self.instance.add_payment(self) - return pay - - -class EditPaymentModalForm(NewPaymentForm): - """ Form handling edit for Payment - - """ - payment = None - - def __init__(self, *args, **kwargs): - self.payment = kwargs.pop("payment", None) - super().__init__(*args, **kwargs) - self.form_title = _("Edit payment") - form_date = { - "amount": self.payment.amount, - "due": str(self.payment.due_on), - "comment": self.payment.comment, - } - self.load_initial_data(form_date, disabled_fields=[]) - - def save(self): - payment = self.payment - payment.amount = self.cleaned_data.get("amount", None) - payment.due_on = self.cleaned_data.get("due", None) - payment.comment = self.cleaned_data.get("comment", None) - payment.save() - self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_EDITED) - self.instance.send_data_to_egon() - return payment - - -class RemovePaymentModalForm(RemoveModalForm): - """ Removing modal form for Payment - - Can be used for anything, where removing shall be confirmed by the user a second time. - - """ - payment = None - - def __init__(self, *args, **kwargs): - payment = kwargs.pop("payment", None) - self.payment = payment - super().__init__(*args, **kwargs) - - def save(self): - self.instance.remove_payment(self) - - -class NewStateModalForm(BaseModalForm): - """ Form handling state related input - - Compensation states refer to 'before' and 'after' states of a compensated surface. Basically it means: - What has been on this area before changes/compensations have been applied and what will be the result ('after')? - - """ - biotope_type = forms.ChoiceField( - label=_("Biotope Type"), - label_suffix="", - required=True, - help_text=_("Select the biotope type"), - widget=CompensationStateTreeRadioSelect(), - ) - biotope_extra = forms.ModelMultipleChoiceField( - label=_("Biotope additional type"), - label_suffix="", - required=False, - help_text=_("Select an additional biotope type"), - queryset=KonovaCode.objects.filter( - is_archived=False, - is_leaf=True, - code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID], - ), - widget=autocomplete.ModelSelect2Multiple( - url="codes-biotope-extra-type-autocomplete", - attrs={ - "data-placeholder": _("Biotope additional type"), - } - ), - ) - surface = forms.DecimalField( - min_value=0.00, - decimal_places=2, - label=_("Surface"), - label_suffix="", - required=True, - help_text=_("in m²"), - widget=forms.NumberInput( - attrs={ - "class": "form-control", - "placeholder": "0,00" - } - ) - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("New state") - self.form_caption = _("Insert data for the new state") - choices = KonovaCode.objects.filter( - code_lists__in=[CODELIST_BIOTOPES_ID], - is_archived=False, - is_leaf=True, - ).values_list("id", flat=True) - choices = [ - (choice, choice) - for choice in choices - ] - self.fields["biotope_type"].choices = choices - - def save(self, is_before_state: bool = False): - state = self.instance.add_state(self, is_before_state) - self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_STATE) - return state - - def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None): - """ Generic processing of request - - Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used - - +++ - The generic method from super class can not be used, since we need to do some request parameter check in here. - +++ - - Args: - request (HttpRequest): The incoming request - msg_success (str): The message in case of successful removing - msg_error (str): The message in case of an error - - Returns: - - """ - redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home") - template = self.template - if request.method == "POST": - if self.is_valid(): - # Modal forms send one POST for checking on data validity. This can be used to return possible errors - # on the form. A second POST (if no errors occured) is sent afterwards and needs to process the - # saving/commiting of the data to the database. is_ajax() performs this check. The first request is - # an ajax call, the second is a regular form POST. - if not is_ajax(request.META): - is_before_state = bool(request.GET.get("before", False)) - self.save(is_before_state=is_before_state) - messages.success( - request, - msg_success - ) - return HttpResponseRedirect(redirect_url) - else: - context = { - "form": self, - } - context = BaseContext(request, context).context - return render(request, template, context) - elif request.method == "GET": - context = { - "form": self, - } - context = BaseContext(request, context).context - return render(request, template, context) - else: - raise NotImplementedError - - -class EditCompensationStateModalForm(NewStateModalForm): - state = None - - def __init__(self, *args, **kwargs): - self.state = kwargs.pop("state", None) - super().__init__(*args, **kwargs) - self.form_title = _("Edit state") - biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None - form_data = { - "biotope_type": biotope_type_id, - "biotope_extra": self.state.biotope_type_details.all(), - "surface": self.state.surface, - } - self.load_initial_data(form_data) - - def save(self, is_before_state: bool = False): - state = self.state - biotope_type_id = self.cleaned_data.get("biotope_type", None) - state.biotope_type = KonovaCode.objects.get(id=biotope_type_id) - state.biotope_type_details.set(self.cleaned_data.get("biotope_extra", [])) - state.surface = self.cleaned_data.get("surface", None) - state.save() - self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_STATE_EDITED) - return state - - -class RemoveCompensationStateModalForm(RemoveModalForm): - """ Removing modal form for CompensationState - - Can be used for anything, where removing shall be confirmed by the user a second time. - - """ - state = None - - def __init__(self, *args, **kwargs): - state = kwargs.pop("state", None) - self.state = state - super().__init__(*args, **kwargs) - - def save(self): - self.instance.remove_state(self) - - -class RemoveCompensationActionModalForm(RemoveModalForm): - """ Removing modal form for CompensationAction - - Can be used for anything, where removing shall be confirmed by the user a second time. - - """ - action = None - - def __init__(self, *args, **kwargs): - action = kwargs.pop("action", None) - self.action = action - super().__init__(*args, **kwargs) - - def save(self): - self.instance.remove_action(self) - - -class NewDeadlineModalForm(BaseModalForm): - """ Form handling deadline related input - - """ - type = forms.ChoiceField( - label=_("Deadline Type"), - label_suffix="", - required=True, - help_text=_("Select the deadline type"), - choices=DeadlineType.choices, - widget=forms.Select( - attrs={ - "class": "form-control" - } - ) - ) - date = forms.DateField( - label=_("Date"), - label_suffix="", - required=True, - help_text=_("Select date"), - widget=forms.DateInput( - attrs={ - "type": "date", - "data-provide": "datepicker", - "class": "form-control", - }, - format="%d.%m.%Y" - ) - ) - comment = forms.CharField( - required=False, - max_length=200, - label=_("Comment"), - label_suffix=_(""), - help_text=_("Additional comment, maximum {} letters").format(200), - widget=forms.Textarea( - attrs={ - "cols": 30, - "rows": 5, - "class": "form-control", - } - ) - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("New deadline") - self.form_caption = _("Insert data for the new deadline") - - def save(self): - deadline = self.instance.add_deadline(self) - return deadline - - -class EditDeadlineModalForm(NewDeadlineModalForm): - deadline = None - - def __init__(self, *args, **kwargs): - self.deadline = kwargs.pop("deadline", None) - super().__init__(*args, **kwargs) - self.form_title = _("Edit deadline") - form_data = { - "type": self.deadline.type, - "date": str(self.deadline.date), - "comment": self.deadline.comment, - } - self.load_initial_data(form_data) - - def save(self): - deadline = self.deadline - deadline.type = self.cleaned_data.get("type", None) - deadline.date = self.cleaned_data.get("date", None) - deadline.comment = self.cleaned_data.get("comment", None) - deadline.save() - self.instance.mark_as_edited(self.user, self.request, edit_comment=DEADLINE_EDITED) - return deadline - - -class NewActionModalForm(BaseModalForm): - """ Form handling action related input - - Compensation actions are the actions performed on the area, which shall be compensated. Actions will change the - surface of the area, the biotopes, and have an environmental impact. With actions the before-after states can change - (not in the process logic in Konova, but in the real world). - - """ - from compensation.models import UnitChoices - action_type = forms.MultipleChoiceField( - label=_("Action Type"), - label_suffix="", - required=True, - help_text=_("An action can consist of multiple different action types. All the selected action types are expected to be performed according to the amount and unit below on this form."), - choices=[], - widget=CompensationActionTreeCheckboxSelectMultiple(), - ) - action_type_details = forms.ModelMultipleChoiceField( - label=_("Action Type detail"), - label_suffix="", - required=False, - help_text=_("Select the action type detail"), - queryset=KonovaCode.objects.filter( - is_archived=False, - is_leaf=True, - code_lists__in=[CODELIST_COMPENSATION_ACTION_DETAIL_ID], - ), - widget=autocomplete.ModelSelect2Multiple( - url="codes-compensation-action-detail-autocomplete", - attrs={ - "data-placeholder": _("Action Type detail"), - } - ), - ) - unit = forms.ChoiceField( - label=_("Unit"), - label_suffix="", - required=True, - help_text=_("Select the unit"), - choices=UnitChoices.choices, - widget=forms.Select( - attrs={ - "class": "form-control" - } - ) - ) - amount = forms.DecimalField( - label=_("Amount"), - label_suffix="", - required=True, - help_text=_("Insert the amount"), - decimal_places=2, - min_value=0.00, - widget=forms.NumberInput( - attrs={ - "class": "form-control", - "placeholder": "0,00", - } - ) - ) - comment = forms.CharField( - required=False, - label=_("Comment"), - label_suffix=_(""), - help_text=_("Additional comment"), - widget=forms.Textarea( - attrs={ - "rows": 5, - "class": "form-control", - } - ) - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("New action") - self.form_caption = _("Insert data for the new action") - choices =KonovaCode.objects.filter( - code_lists__in=[CODELIST_COMPENSATION_ACTION_ID], - is_archived=False, - is_leaf=True, - ).values_list("id", flat=True) - choices = [ - (choice, choice) - for choice in choices - ] - self.fields["action_type"].choices = choices - - def save(self): - action = self.instance.add_action(self) - self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_ACTION) - return action - - -class EditCompensationActionModalForm(NewActionModalForm): - action = None - - def __init__(self, *args, **kwargs): - self.action = kwargs.pop("action", None) - super().__init__(*args, **kwargs) - self.form_title = _("Edit action") - form_data = { - "action_type": list(self.action.action_type.values_list("id", flat=True)), - "action_type_details": self.action.action_type_details.all(), - "amount": self.action.amount, - "unit": self.action.unit, - "comment": self.action.comment, - } - self.load_initial_data(form_data) - - def save(self): - action = self.action - action.action_type.set(self.cleaned_data.get("action_type", [])) - action.action_type_details.set(self.cleaned_data.get("action_type_details", [])) - action.amount = self.cleaned_data.get("amount", None) - action.unit = self.cleaned_data.get("unit", None) - action.comment = self.cleaned_data.get("comment", None) - action.save() - self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_ACTION_EDITED) - return action - - -class NewCompensationDocumentModalForm(NewDocumentModalForm): - document_model = CompensationDocument - - -class NewEcoAccountDocumentModalForm(NewDocumentModalForm): - document_model = EcoAccountDocument \ No newline at end of file diff --git a/compensation/forms/modals/__init__.py b/compensation/forms/modals/__init__.py new file mode 100644 index 00000000..ca978536 --- /dev/null +++ b/compensation/forms/modals/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" diff --git a/compensation/forms/modals/compensation_action.py b/compensation/forms/modals/compensation_action.py new file mode 100644 index 00000000..cc0bae15 --- /dev/null +++ b/compensation/forms/modals/compensation_action.py @@ -0,0 +1,155 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from dal import autocomplete +from django import forms +from django.utils.translation import gettext_lazy as _ + +from codelist.models import KonovaCode +from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID +from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple +from konova.forms.modals import BaseModalForm, RemoveModalForm +from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, ADDED_COMPENSATION_ACTION + + +class NewCompensationActionModalForm(BaseModalForm): + """ Form handling action related input + + Compensation actions are the actions performed on the area, which shall be compensated. Actions will change the + surface of the area, the biotopes, and have an environmental impact. With actions the before-after states can change + (not in the process logic in Konova, but in the real world). + + """ + from compensation.models import UnitChoices + action_type = forms.MultipleChoiceField( + label=_("Action Type"), + label_suffix="", + required=True, + help_text=_("An action can consist of multiple different action types. All the selected action types are expected to be performed according to the amount and unit below on this form."), + choices=[], + widget=CompensationActionTreeCheckboxSelectMultiple(), + ) + action_type_details = forms.ModelMultipleChoiceField( + label=_("Action Type detail"), + label_suffix="", + required=False, + help_text=_("Select the action type detail"), + queryset=KonovaCode.objects.filter( + is_archived=False, + is_leaf=True, + code_lists__in=[CODELIST_COMPENSATION_ACTION_DETAIL_ID], + ), + widget=autocomplete.ModelSelect2Multiple( + url="codes-compensation-action-detail-autocomplete", + attrs={ + "data-placeholder": _("Action Type detail"), + } + ), + ) + unit = forms.ChoiceField( + label=_("Unit"), + label_suffix="", + required=True, + help_text=_("Select the unit"), + choices=UnitChoices.choices, + widget=forms.Select( + attrs={ + "class": "form-control" + } + ) + ) + amount = forms.DecimalField( + label=_("Amount"), + label_suffix="", + required=True, + help_text=_("Insert the amount"), + decimal_places=2, + min_value=0.00, + widget=forms.NumberInput( + attrs={ + "class": "form-control", + "placeholder": "0,00", + } + ) + ) + comment = forms.CharField( + required=False, + label=_("Comment"), + label_suffix=_(""), + help_text=_("Additional comment"), + widget=forms.Textarea( + attrs={ + "rows": 5, + "class": "form-control", + } + ) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("New action") + self.form_caption = _("Insert data for the new action") + choices =KonovaCode.objects.filter( + code_lists__in=[CODELIST_COMPENSATION_ACTION_ID], + is_archived=False, + is_leaf=True, + ).values_list("id", flat=True) + choices = [ + (choice, choice) + for choice in choices + ] + self.fields["action_type"].choices = choices + + def save(self): + action = self.instance.add_action(self) + self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_ACTION) + return action + + +class EditCompensationActionModalForm(NewCompensationActionModalForm): + action = None + + def __init__(self, *args, **kwargs): + self.action = kwargs.pop("action", None) + super().__init__(*args, **kwargs) + self.form_title = _("Edit action") + form_data = { + "action_type": list(self.action.action_type.values_list("id", flat=True)), + "action_type_details": self.action.action_type_details.all(), + "amount": self.action.amount, + "unit": self.action.unit, + "comment": self.action.comment, + } + self.load_initial_data(form_data) + + def save(self): + action = self.action + action.action_type.set(self.cleaned_data.get("action_type", [])) + action.action_type_details.set(self.cleaned_data.get("action_type_details", [])) + action.amount = self.cleaned_data.get("amount", None) + action.unit = self.cleaned_data.get("unit", None) + action.comment = self.cleaned_data.get("comment", None) + action.save() + self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_ACTION_EDITED) + return action + + +class RemoveCompensationActionModalForm(RemoveModalForm): + """ Removing modal form for CompensationAction + + Can be used for anything, where removing shall be confirmed by the user a second time. + + """ + action = None + + def __init__(self, *args, **kwargs): + action = kwargs.pop("action", None) + self.action = action + super().__init__(*args, **kwargs) + + def save(self): + self.instance.remove_action(self) \ No newline at end of file diff --git a/compensation/forms/modals/deadline.py b/compensation/forms/modals/deadline.py new file mode 100644 index 00000000..330c83c8 --- /dev/null +++ b/compensation/forms/modals/deadline.py @@ -0,0 +1,92 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.utils.translation import gettext_lazy as _ + +from konova.forms.modals import BaseModalForm +from konova.models import DeadlineType +from konova.utils.message_templates import DEADLINE_EDITED + + +class NewDeadlineModalForm(BaseModalForm): + """ Form handling deadline related input + + """ + type = forms.ChoiceField( + label=_("Deadline Type"), + label_suffix="", + required=True, + help_text=_("Select the deadline type"), + choices=DeadlineType.choices, + widget=forms.Select( + attrs={ + "class": "form-control" + } + ) + ) + date = forms.DateField( + label=_("Date"), + label_suffix="", + required=True, + help_text=_("Select date"), + widget=forms.DateInput( + attrs={ + "type": "date", + "data-provide": "datepicker", + "class": "form-control", + }, + format="%d.%m.%Y" + ) + ) + comment = forms.CharField( + required=False, + max_length=200, + label=_("Comment"), + label_suffix=_(""), + help_text=_("Additional comment, maximum {} letters").format(200), + widget=forms.Textarea( + attrs={ + "cols": 30, + "rows": 5, + "class": "form-control", + } + ) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("New deadline") + self.form_caption = _("Insert data for the new deadline") + + def save(self): + deadline = self.instance.add_deadline(self) + return deadline + + +class EditDeadlineModalForm(NewDeadlineModalForm): + deadline = None + + def __init__(self, *args, **kwargs): + self.deadline = kwargs.pop("deadline", None) + super().__init__(*args, **kwargs) + self.form_title = _("Edit deadline") + form_data = { + "type": self.deadline.type, + "date": str(self.deadline.date), + "comment": self.deadline.comment, + } + self.load_initial_data(form_data) + + def save(self): + deadline = self.deadline + deadline.type = self.cleaned_data.get("type", None) + deadline.date = self.cleaned_data.get("date", None) + deadline.comment = self.cleaned_data.get("comment", None) + deadline.save() + self.instance.mark_as_edited(self.user, self.request, edit_comment=DEADLINE_EDITED) + return deadline diff --git a/compensation/forms/modals/document.py b/compensation/forms/modals/document.py new file mode 100644 index 00000000..83c2ff92 --- /dev/null +++ b/compensation/forms/modals/document.py @@ -0,0 +1,17 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from compensation.models import CompensationDocument, EcoAccountDocument +from konova.forms.modals import NewDocumentModalForm + + +class NewCompensationDocumentModalForm(NewDocumentModalForm): + document_model = CompensationDocument + + +class NewEcoAccountDocumentModalForm(NewDocumentModalForm): + document_model = EcoAccountDocument diff --git a/compensation/forms/modals/payment.py b/compensation/forms/modals/payment.py new file mode 100644 index 00000000..4383a576 --- /dev/null +++ b/compensation/forms/modals/payment.py @@ -0,0 +1,136 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _ + +from konova.forms.modals import RemoveModalForm, BaseModalForm +from konova.utils.message_templates import PAYMENT_EDITED + + +class NewPaymentForm(BaseModalForm): + """ Form handling payment related input + + """ + amount = forms.DecimalField( + min_value=0.00, + decimal_places=2, + label=_con("money", "Amount"), # contextual translation + label_suffix=_(""), + help_text=_("in Euro"), + widget=forms.NumberInput( + attrs={ + "class": "form-control", + "placeholder": "0,00", + } + ) + ) + due = forms.DateField( + label=_("Due on"), + label_suffix=_(""), + required=False, + help_text=_("Due on which date"), + widget=forms.DateInput( + attrs={ + "type": "date", + "data-provide": "datepicker", + "class": "form-control", + }, + format="%d.%m.%Y" + ) + ) + comment = forms.CharField( + max_length=200, + required=False, + label=_("Comment"), + label_suffix=_(""), + help_text=_("Additional comment, maximum {} letters").format(200), + widget=forms.Textarea( + attrs={ + "rows": 5, + "class": "form-control" + } + ) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.intervention = self.instance + self.form_title = _("Payment") + self.form_caption = _("Add a payment for intervention '{}'").format(self.intervention.title) + + def is_valid(self): + """ + Checks on form validity. + + For this form we need to make sure that a date or a comment is set. + If both are missing, the user needs to enter at least an explanation why + there is no date to be entered. + + Returns: + is_valid (bool): True if valid, False otherwise + """ + super_valid = super().is_valid() + date = self.cleaned_data["due"] + comment = self.cleaned_data["comment"] or None + if not date and not comment: + # At least one needs to be set! + self.add_error( + "comment", + _("If there is no date you can enter, please explain why.") + ) + return False + return super_valid + + def save(self): + pay = self.instance.add_payment(self) + return pay + + +class EditPaymentModalForm(NewPaymentForm): + """ Form handling edit for Payment + + """ + payment = None + + def __init__(self, *args, **kwargs): + self.payment = kwargs.pop("payment", None) + super().__init__(*args, **kwargs) + self.form_title = _("Edit payment") + form_date = { + "amount": self.payment.amount, + "due": str(self.payment.due_on), + "comment": self.payment.comment, + } + self.load_initial_data(form_date, disabled_fields=[]) + + def save(self): + payment = self.payment + payment.amount = self.cleaned_data.get("amount", None) + payment.due_on = self.cleaned_data.get("due", None) + payment.comment = self.cleaned_data.get("comment", None) + payment.save() + self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_EDITED) + self.instance.send_data_to_egon() + return payment + + +class RemovePaymentModalForm(RemoveModalForm): + """ Removing modal form for Payment + + Can be used for anything, where removing shall be confirmed by the user a second time. + + """ + payment = None + + def __init__(self, *args, **kwargs): + payment = kwargs.pop("payment", None) + self.payment = payment + super().__init__(*args, **kwargs) + + def save(self): + self.instance.remove_payment(self) diff --git a/compensation/forms/modals/state.py b/compensation/forms/modals/state.py new file mode 100644 index 00000000..485045ba --- /dev/null +++ b/compensation/forms/modals/state.py @@ -0,0 +1,179 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from bootstrap_modal_forms.utils import is_ajax +from dal import autocomplete +from django import forms +from django.contrib import messages +from django.http import HttpResponseRedirect, HttpRequest +from django.shortcuts import render +from django.utils.translation import gettext_lazy as _ + +from codelist.models import KonovaCode +from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID +from intervention.inputs import CompensationStateTreeRadioSelect +from konova.contexts import BaseContext +from konova.forms.modals import RemoveModalForm, BaseModalForm +from konova.utils.message_templates import COMPENSATION_STATE_EDITED, FORM_INVALID, ADDED_COMPENSATION_STATE + + +class NewCompensationStateModalForm(BaseModalForm): + """ Form handling state related input + + Compensation states refer to 'before' and 'after' states of a compensated surface. Basically it means: + What has been on this area before changes/compensations have been applied and what will be the result ('after')? + + """ + biotope_type = forms.ChoiceField( + label=_("Biotope Type"), + label_suffix="", + required=True, + help_text=_("Select the biotope type"), + widget=CompensationStateTreeRadioSelect(), + ) + biotope_extra = forms.ModelMultipleChoiceField( + label=_("Biotope additional type"), + label_suffix="", + required=False, + help_text=_("Select an additional biotope type"), + queryset=KonovaCode.objects.filter( + is_archived=False, + is_leaf=True, + code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID], + ), + widget=autocomplete.ModelSelect2Multiple( + url="codes-biotope-extra-type-autocomplete", + attrs={ + "data-placeholder": _("Biotope additional type"), + } + ), + ) + surface = forms.DecimalField( + min_value=0.00, + decimal_places=2, + label=_("Surface"), + label_suffix="", + required=True, + help_text=_("in m²"), + widget=forms.NumberInput( + attrs={ + "class": "form-control", + "placeholder": "0,00" + } + ) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("New state") + self.form_caption = _("Insert data for the new state") + choices = KonovaCode.objects.filter( + code_lists__in=[CODELIST_BIOTOPES_ID], + is_archived=False, + is_leaf=True, + ).values_list("id", flat=True) + choices = [ + (choice, choice) + for choice in choices + ] + self.fields["biotope_type"].choices = choices + + def save(self, is_before_state: bool = False): + state = self.instance.add_state(self, is_before_state) + self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_STATE) + return state + + def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None): + """ Generic processing of request + + Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used + + +++ + The generic method from super class can not be used, since we need to do some request parameter check in here. + +++ + + Args: + request (HttpRequest): The incoming request + msg_success (str): The message in case of successful removing + msg_error (str): The message in case of an error + + Returns: + + """ + redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home") + template = self.template + if request.method == "POST": + if self.is_valid(): + # Modal forms send one POST for checking on data validity. This can be used to return possible errors + # on the form. A second POST (if no errors occured) is sent afterwards and needs to process the + # saving/commiting of the data to the database. is_ajax() performs this check. The first request is + # an ajax call, the second is a regular form POST. + if not is_ajax(request.META): + is_before_state = bool(request.GET.get("before", False)) + self.save(is_before_state=is_before_state) + messages.success( + request, + msg_success + ) + return HttpResponseRedirect(redirect_url) + else: + context = { + "form": self, + } + context = BaseContext(request, context).context + return render(request, template, context) + elif request.method == "GET": + context = { + "form": self, + } + context = BaseContext(request, context).context + return render(request, template, context) + else: + raise NotImplementedError + + +class EditCompensationStateModalForm(NewCompensationStateModalForm): + state = None + + def __init__(self, *args, **kwargs): + self.state = kwargs.pop("state", None) + super().__init__(*args, **kwargs) + self.form_title = _("Edit state") + biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None + form_data = { + "biotope_type": biotope_type_id, + "biotope_extra": self.state.biotope_type_details.all(), + "surface": self.state.surface, + } + self.load_initial_data(form_data) + + def save(self, is_before_state: bool = False): + state = self.state + biotope_type_id = self.cleaned_data.get("biotope_type", None) + state.biotope_type = KonovaCode.objects.get(id=biotope_type_id) + state.biotope_type_details.set(self.cleaned_data.get("biotope_extra", [])) + state.surface = self.cleaned_data.get("surface", None) + state.save() + self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_STATE_EDITED) + return state + + +class RemoveCompensationStateModalForm(RemoveModalForm): + """ Removing modal form for CompensationState + + Can be used for anything, where removing shall be confirmed by the user a second time. + + """ + state = None + + def __init__(self, *args, **kwargs): + state = kwargs.pop("state", None) + self.state = state + super().__init__(*args, **kwargs) + + def save(self): + self.instance.remove_state(self) diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py index db01a045..e12324ce 100644 --- a/compensation/views/compensation.py +++ b/compensation/views/compensation.py @@ -5,10 +5,12 @@ from django.http import HttpRequest, JsonResponse from django.shortcuts import render from django.utils.translation import gettext_lazy as _ -from compensation.forms.forms import NewCompensationForm, EditCompensationForm -from compensation.forms.modalForms import NewStateModalForm, NewDeadlineModalForm, NewActionModalForm, \ - NewCompensationDocumentModalForm, RemoveCompensationActionModalForm, RemoveCompensationStateModalForm, \ - EditCompensationStateModalForm, EditCompensationActionModalForm, EditDeadlineModalForm +from compensation.forms.compensation import NewCompensationForm, EditCompensationForm +from compensation.forms.modals.state import NewCompensationStateModalForm, RemoveCompensationStateModalForm, EditCompensationStateModalForm +from compensation.forms.modals.document import NewCompensationDocumentModalForm +from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \ + EditCompensationActionModalForm, RemoveCompensationActionModalForm +from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm from compensation.models import Compensation, CompensationState, CompensationAction, CompensationDocument from compensation.tables import CompensationTable from intervention.models import Intervention @@ -21,7 +23,7 @@ from konova.models import Deadline from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.documents import get_document, remove_document from konova.utils.generators import generate_qr_code -from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, DATA_UNSHARED_EXPLANATION, \ +from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, \ CHECKED_RECORDED_RESET, COMPENSATION_ADDED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, \ COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, \ DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, \ @@ -402,7 +404,7 @@ def state_new_view(request: HttpRequest, id: str): """ comp = get_object_or_404(Compensation, id=id) - form = NewStateModalForm(request.POST or None, instance=comp, request=request) + form = NewCompensationStateModalForm(request.POST or None, instance=comp, request=request) return form.process_request( request, msg_success=COMPENSATION_STATE_ADDED, @@ -424,7 +426,7 @@ def action_new_view(request: HttpRequest, id: str): """ comp = get_object_or_404(Compensation, id=id) - form = NewActionModalForm(request.POST or None, instance=comp, request=request) + form = NewCompensationActionModalForm(request.POST or None, instance=comp, request=request) return form.process_request( request, msg_success=COMPENSATION_ACTION_ADDED, diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index 2ebeb1f7..d600c23c 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -14,10 +14,13 @@ from django.core.exceptions import ObjectDoesNotExist from django.http import HttpRequest, Http404, JsonResponse from django.shortcuts import render, get_object_or_404, redirect -from compensation.forms.forms import NewEcoAccountForm, EditEcoAccountForm -from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm, \ - NewEcoAccountDocumentModalForm, RemoveCompensationActionModalForm, RemoveCompensationStateModalForm, \ - EditCompensationStateModalForm, EditCompensationActionModalForm, EditDeadlineModalForm +from compensation.forms.eco_account import NewEcoAccountForm, EditEcoAccountForm +from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \ + RemoveCompensationActionModalForm, EditCompensationActionModalForm +from compensation.forms.modals.deadline import EditDeadlineModalForm, NewDeadlineModalForm +from compensation.forms.modals.document import NewEcoAccountDocumentModalForm +from compensation.forms.modals.state import NewCompensationStateModalForm, RemoveCompensationStateModalForm, \ + EditCompensationStateModalForm from compensation.models import EcoAccount, EcoAccountDocument, CompensationState, CompensationAction from compensation.tables import EcoAccountTable from intervention.forms.modalForms import NewDeductionModalForm, ShareModalForm, RemoveEcoAccountDeductionModalForm, \ @@ -401,7 +404,7 @@ def state_new_view(request: HttpRequest, id: str): """ acc = get_object_or_404(EcoAccount, id=id) - form = NewStateModalForm(request.POST or None, instance=acc, request=request) + form = NewCompensationStateModalForm(request.POST or None, instance=acc, request=request) return form.process_request( request, msg_success=COMPENSATION_STATE_ADDED, @@ -423,7 +426,7 @@ def action_new_view(request: HttpRequest, id: str): """ acc = get_object_or_404(EcoAccount, id=id) - form = NewActionModalForm(request.POST or None, instance=acc, request=request) + form = NewCompensationActionModalForm(request.POST or None, instance=acc, request=request) return form.process_request( request, msg_success=COMPENSATION_ACTION_ADDED, diff --git a/compensation/views/payment.py b/compensation/views/payment.py index 84fad5bc..29e89ac3 100644 --- a/compensation/views/payment.py +++ b/compensation/views/payment.py @@ -6,12 +6,11 @@ Created on: 09.08.21 """ from django.urls import reverse -from django.utils.translation import gettext_lazy as _ from django.contrib.auth.decorators import login_required from django.http import HttpRequest from django.shortcuts import get_object_or_404 -from compensation.forms.modalForms import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm +from compensation.forms.modals.payment import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm from compensation.models import Payment from intervention.models import Intervention from konova.decorators import default_group_required, shared_access_required diff --git a/ema/forms.py b/ema/forms.py index 93f23490..bbe09a88 100644 --- a/ema/forms.py +++ b/ema/forms.py @@ -10,8 +10,8 @@ from django.db import transaction from django.urls import reverse, reverse_lazy from django.utils.translation import gettext_lazy as _ -from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin, \ - PikCompensationFormMixin +from compensation.forms.mixins import CompensationResponsibleFormMixin, PikCompensationFormMixin +from compensation.forms.compensation import AbstractCompensationForm from ema.models import Ema, EmaDocument from intervention.models import Responsibility, Handler from konova.forms import SimpleGeomForm diff --git a/ema/views.py b/ema/views.py index f07187aa..fea6d4bc 100644 --- a/ema/views.py +++ b/ema/views.py @@ -6,9 +6,11 @@ from django.shortcuts import render, get_object_or_404, redirect from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm, \ - RemoveCompensationActionModalForm, RemoveCompensationStateModalForm, EditCompensationStateModalForm, \ - EditCompensationActionModalForm, EditDeadlineModalForm +from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \ + EditCompensationActionModalForm, RemoveCompensationActionModalForm +from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm +from compensation.forms.modals.state import NewCompensationStateModalForm, RemoveCompensationStateModalForm, \ + EditCompensationStateModalForm from compensation.models import CompensationAction, CompensationState from ema.forms import NewEmaForm, EditEmaForm, NewEmaDocumentModalForm from ema.tables import EmaTable @@ -303,7 +305,7 @@ def state_new_view(request: HttpRequest, id: str): """ ema = get_object_or_404(Ema, id=id) - form = NewStateModalForm(request.POST or None, instance=ema, request=request) + form = NewCompensationStateModalForm(request.POST or None, instance=ema, request=request) return form.process_request( request, msg_success=COMPENSATION_STATE_ADDED, @@ -325,7 +327,7 @@ def action_new_view(request: HttpRequest, id: str): """ ema = get_object_or_404(Ema, id=id) - form = NewActionModalForm(request.POST or None, instance=ema, request=request) + form = NewCompensationActionModalForm(request.POST or None, instance=ema, request=request) return form.process_request( request, msg_success=COMPENSATION_ACTION_ADDED, From de8d79983d6e9962797fe6b211e3a066206b289c Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 18 Aug 2022 10:08:51 +0200 Subject: [PATCH 12/48] Intervention forms * refactors intervention/forms and ../modalForms into individual files in separated packages * forms.py has been renamed into intervention.py, now can be found as intervention/forms/intervention.py * modalForms.py has been split into individual files living in modals package, can be found as intervention/forms/modals/... --- codelist/models.py | 2 +- compensation/views/eco_account.py | 5 +- ema/views.py | 2 +- .../forms/{forms.py => intervention.py} | 0 intervention/forms/modalForms.py | 574 ------------------ intervention/forms/modals/__init__.py | 7 + intervention/forms/modals/check.py | 107 ++++ intervention/forms/modals/deduction.py | 252 ++++++++ intervention/forms/modals/document.py | 13 + intervention/forms/modals/revocation.py | 110 ++++ intervention/forms/modals/share.py | 136 +++++ intervention/views.py | 14 +- konova/forms/base_form.py | 4 +- 13 files changed, 641 insertions(+), 585 deletions(-) rename intervention/forms/{forms.py => intervention.py} (100%) delete mode 100644 intervention/forms/modalForms.py create mode 100644 intervention/forms/modals/__init__.py create mode 100644 intervention/forms/modals/check.py create mode 100644 intervention/forms/modals/deduction.py create mode 100644 intervention/forms/modals/document.py create mode 100644 intervention/forms/modals/revocation.py create mode 100644 intervention/forms/modals/share.py diff --git a/codelist/models.py b/codelist/models.py index 8e5de0c9..5d06172e 100644 --- a/codelist/models.py +++ b/codelist/models.py @@ -50,7 +50,7 @@ class KonovaCode(models.Model): def __str__(self, with_parent: bool = True): ret_val = "" - if self.parent and with_parent: + if self.parent and self.parent.long_name and with_parent: ret_val += self.parent.long_name + " > " ret_val += self.long_name if self.short_name and self.short_name != self.long_name: diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index d600c23c..07796d14 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -23,8 +23,9 @@ from compensation.forms.modals.state import NewCompensationStateModalForm, Remov EditCompensationStateModalForm from compensation.models import EcoAccount, EcoAccountDocument, CompensationState, CompensationAction from compensation.tables import EcoAccountTable -from intervention.forms.modalForms import NewDeductionModalForm, ShareModalForm, RemoveEcoAccountDeductionModalForm, \ +from intervention.forms.modals.deduction import RemoveEcoAccountDeductionModalForm, NewEcoAccountDeductionModalForm, \ EditEcoAccountDeductionModalForm +from intervention.forms.modals.share import ShareModalForm from konova.contexts import BaseContext from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \ shared_access_required @@ -707,7 +708,7 @@ def new_deduction_view(request: HttpRequest, id: str): acc = get_object_or_404(EcoAccount, id=id) if not acc.recorded: raise Http404() - form = NewDeductionModalForm(request.POST or None, instance=acc, request=request) + form = NewEcoAccountDeductionModalForm(request.POST or None, instance=acc, request=request) return form.process_request( request, msg_success=DEDUCTION_ADDED, diff --git a/ema/views.py b/ema/views.py index fea6d4bc..2dd6e51f 100644 --- a/ema/views.py +++ b/ema/views.py @@ -14,7 +14,7 @@ from compensation.forms.modals.state import NewCompensationStateModalForm, Remov from compensation.models import CompensationAction, CompensationState from ema.forms import NewEmaForm, EditEmaForm, NewEmaDocumentModalForm from ema.tables import EmaTable -from intervention.forms.modalForms import ShareModalForm +from intervention.forms.modals.share import ShareModalForm from konova.contexts import BaseContext from konova.decorators import conservation_office_group_required, shared_access_required from ema.models import Ema, EmaDocument diff --git a/intervention/forms/forms.py b/intervention/forms/intervention.py similarity index 100% rename from intervention/forms/forms.py rename to intervention/forms/intervention.py diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py deleted file mode 100644 index a977c1ce..00000000 --- a/intervention/forms/modalForms.py +++ /dev/null @@ -1,574 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 27.09.21 - -""" -from dal import autocomplete -from django.core.exceptions import ObjectDoesNotExist - -from konova.utils.message_templates import DEDUCTION_ADDED, REVOCATION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED, \ - REVOCATION_EDITED, ENTRY_REMOVE_MISSING_PERMISSION -from user.models import User, Team -from user.models import UserActionLogEntry -from django.db import transaction -from django import forms -from django.utils.translation import gettext_lazy as _ - -from compensation.models import EcoAccount, EcoAccountDeduction -from intervention.inputs import TextToClipboardInput -from intervention.models import Intervention, InterventionDocument, RevocationDocument -from konova.forms.modals import BaseModalForm -from konova.forms.modals import NewDocumentModalForm, RemoveModalForm -from konova.utils.general import format_german_float -from konova.utils.user_checks import is_default_group_only - - -class ShareModalForm(BaseModalForm): - url = forms.CharField( - label=_("Share link"), - label_suffix="", - help_text=_("Send this link to users who you want to have writing access on the data"), - required=False, - widget=TextToClipboardInput( - attrs={ - "readonly": True, - "class": "form-control", - } - ) - ) - teams = forms.ModelMultipleChoiceField( - label=_("Add team to share with"), - label_suffix="", - help_text=_("Multiple selection possible - You can only select teams which do not already have access."), - required=False, - queryset=Team.objects.all(), - widget=autocomplete.ModelSelect2Multiple( - url="share-team-autocomplete", - attrs={ - "data-placeholder": _("Click for selection"), - "data-minimum-input-length": 3, - }, - ), - ) - users = forms.ModelMultipleChoiceField( - label=_("Add user to share with"), - label_suffix="", - help_text=_("Multiple selection possible - You can only select users which do not already have access. Enter the full username."), - required=False, - queryset=User.objects.all(), - widget=autocomplete.ModelSelect2Multiple( - url="share-user-autocomplete", - attrs={ - "data-placeholder": _("Click for selection"), - "data-minimum-input-length": 3, - }, - ), - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Share") - self.form_caption = _("Share settings for {}").format(self.instance.identifier) - self.template = "modal/modal_form.html" - - # Make sure an access_token is set - if self.instance.access_token is None: - self.instance.generate_access_token() - - self._init_fields() - - def _user_team_valid(self): - """ Checks whether users and teams have been removed by the user and if the user is allowed to do so or not - - Returns: - - """ - users = self.cleaned_data.get("users", User.objects.none()) - teams = self.cleaned_data.get("teams", Team.objects.none()) - - _is_valid = True - if is_default_group_only(self.user): - shared_users = self.instance.shared_users - shared_teams = self.instance.shared_teams - - shared_users_are_removed = not set(shared_users).issubset(users) - shared_teams_are_removed = not set(shared_teams).issubset(teams) - - if shared_users_are_removed: - self.add_error( - "users", - ENTRY_REMOVE_MISSING_PERMISSION - ) - _is_valid = False - if shared_teams_are_removed: - self.add_error( - "teams", - ENTRY_REMOVE_MISSING_PERMISSION - ) - _is_valid = False - return _is_valid - - def is_valid(self): - """ Extended validity check - - Returns: - - """ - super_valid = super().is_valid() - user_team_valid = self._user_team_valid() - _is_valid = super_valid and user_team_valid - return _is_valid - - def _init_fields(self): - """ Wraps initializing of fields - - Returns: - - """ - # Initialize share_link field - share_link = self.instance.get_share_link() - self.share_link = self.request.build_absolute_uri(share_link) - self.initialize_form_field( - "url", - self.share_link - ) - - form_data = { - "teams": self.instance.teams.all(), - "users": self.instance.users.all(), - } - self.load_initial_data(form_data) - - def save(self): - self.instance.update_shared_access(self) - - -class NewRevocationModalForm(BaseModalForm): - date = forms.DateField( - label=_("Date"), - label_suffix=_(""), - help_text=_("Date of revocation"), - widget=forms.DateInput( - attrs={ - "type": "date", - "data-provide": "datepicker", - "class": "form-control", - }, - format="%d.%m.%Y" - ) - ) - file = forms.FileField( - label=_("Document"), - label_suffix=_(""), - required=False, - help_text=_("Must be smaller than 15 Mb"), - widget=forms.FileInput( - attrs={ - "class": "form-control-file" - } - ) - ) - comment = forms.CharField( - required=False, - max_length=200, - label=_("Comment"), - label_suffix=_(""), - help_text=_("Additional comment, maximum {} letters").format(200), - widget=forms.Textarea( - attrs={ - "cols": 30, - "rows": 5, - "class": "form-control", - } - ) - ) - document_model = RevocationDocument - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Add revocation") - self.form_caption = "" - self.form_attrs = { - "enctype": "multipart/form-data", # important for file upload - } - - def save(self): - revocation = self.instance.add_revocation(self) - self.instance.mark_as_edited(self.user, self.request, edit_comment=REVOCATION_ADDED) - return revocation - - -class EditRevocationModalForm(NewRevocationModalForm): - revocation = None - - def __init__(self, *args, **kwargs): - self.revocation = kwargs.pop("revocation", None) - super().__init__(*args, **kwargs) - self.form_title = _("Edit revocation") - try: - doc = self.revocation.document.file - except ObjectDoesNotExist: - doc = None - form_data = { - "date": str(self.revocation.date), - "file": doc, - "comment": self.revocation.comment, - } - self.load_initial_data(form_data) - - def save(self): - revocation = self.instance.edit_revocation(self) - self.instance.mark_as_edited(self.user, self.request, edit_comment=REVOCATION_EDITED) - return revocation - - -class RemoveRevocationModalForm(RemoveModalForm): - """ Removing modal form for Revocation - - Can be used for anything, where removing shall be confirmed by the user a second time. - - """ - revocation = None - - def __init__(self, *args, **kwargs): - revocation = kwargs.pop("revocation", None) - self.revocation = revocation - super().__init__(*args, **kwargs) - - def save(self): - self.instance.remove_revocation(self) - - -class CheckModalForm(BaseModalForm): - """ The modal form for running a check on interventions and their compensations - - """ - checked_intervention = forms.BooleanField( - label=_("Checked intervention data"), - label_suffix="", - widget=forms.CheckboxInput(), - required=True, - ) - checked_comps = forms.BooleanField( - label=_("Checked compensations data and payments"), - label_suffix="", - widget=forms.CheckboxInput(), - required=True - ) - valid = None - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Run check") - self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) - self.valid = False - - def _are_deductions_valid(self): - """ Performs validity checks on deductions and their eco-account - - Returns: - - """ - deductions = self.instance.deductions.all() - for deduction in deductions: - checker = deduction.account.quality_check() - for msg in checker.messages: - self.add_error( - "checked_comps", - f"{deduction.account.identifier}: {msg}" - ) - return checker.valid - return True - - def _are_comps_valid(self): - """ Performs validity checks on all types of compensations - - Types of compensations are - * regular Compensations - * deductions from EcoAccounts - - Returns: - - """ - comps = self.instance.compensations.filter( - deleted=None, - ) - comps_valid = True - for comp in comps: - checker = comp.quality_check() - for msg in checker.messages: - self.add_error( - "checked_comps", - f"{comp.identifier}: {msg}" - ) - comps_valid = checker.valid - deductions_valid = self._are_deductions_valid() - return deductions_valid and comps_valid - - def is_valid(self) -> bool: - """ Perform a validity check based on quality_check() logic - - Returns: - result (bool) - """ - super_valid = super().is_valid() - # Perform check - checker = self.instance.quality_check() - for msg in checker.messages: - self.add_error( - "checked_intervention", - msg - ) - all_comps_valid = self._are_comps_valid() - intervention_valid = checker.valid - - return super_valid and intervention_valid and all_comps_valid - - def save(self): - """ Saving logic - - Returns: - - """ - with transaction.atomic(): - self.instance.set_checked(self.user) - - -class NewDeductionModalForm(BaseModalForm): - """ Form for creating new deduction - - Can be used for Intervention view as well as for EcoAccount views. - - Parameter 'instance' can be an intervention, as well as an ecoAccount. - An instance check handles both workflows properly. - - """ - account = forms.ModelChoiceField( - label=_("Eco-account"), - label_suffix="", - help_text=_("Only recorded accounts can be selected for deductions"), - queryset=EcoAccount.objects.filter(deleted=None), - widget=autocomplete.ModelSelect2( - url="accounts-autocomplete", - attrs={ - "data-placeholder": _("Eco-account"), - "data-minimum-input-length": 3, - "readonly": True, - } - ), - ) - surface = forms.DecimalField( - min_value=0.00, - decimal_places=2, - label=_("Surface"), - label_suffix="", - help_text=_("in m²"), - widget=forms.NumberInput( - attrs={ - "class": "form-control", - "placeholder": "0,00", - } - ) - ) - intervention = forms.ModelChoiceField( - label=_("Intervention"), - label_suffix="", - help_text=_("Only shared interventions can be selected"), - queryset=Intervention.objects.filter(deleted=None), - widget=autocomplete.ModelSelect2( - url="interventions-autocomplete", - attrs={ - "data-placeholder": _("Intervention"), - "data-minimum-input-length": 3, - } - ), - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("New Deduction") - self.form_caption = _("Enter the information for a new deduction from a chosen eco-account") - - # Check for Intervention or EcoAccount - if isinstance(self.instance, Intervention): - # Form has been called with a given intervention - self.initialize_form_field("intervention", self.instance) - self.disable_form_field("intervention") - elif isinstance(self.instance, EcoAccount): - # Form has been called with a given account --> make it initial in the form and read-only - self.initialize_form_field("account", self.instance) - self.disable_form_field("account") - else: - raise NotImplementedError - - def _get_available_surface(self, acc): - """ Calculates how much available surface is left on the account - - Args: - acc (EcoAccount): - - Returns: - - """ - # Calculate valid surface - deductable_surface = acc.deductable_surface - sum_surface_deductions = acc.get_deductions_surface() - rest_surface = deductable_surface - sum_surface_deductions - return rest_surface - - def is_valid(self): - """ Custom validity check - - Makes sure the deduction can not contain more surface than the account still provides - - Returns: - is_valid (bool) - """ - super_result = super().is_valid() - acc = self.cleaned_data["account"] - intervention = self.cleaned_data["intervention"] - objects_valid = True - - if not acc.recorded: - self.add_error( - "account", - _("Eco-account {} is not recorded yet. You can only deduct from recorded accounts.").format(acc.identifier) - ) - objects_valid = False - - if intervention.is_recorded: - self.add_error( - "intervention", - _("Intervention {} is currently recorded. To change any data on it, the entry must be unrecorded.").format(intervention.identifier) - ) - objects_valid = False - - rest_surface = self._get_available_surface(acc) - form_surface = float(self.cleaned_data["surface"]) - is_valid_surface = form_surface <= rest_surface - if not is_valid_surface: - self.add_error( - "surface", - _("The account {} has not enough surface for a deduction of {} m². There are only {} m² left").format( - acc.identifier, - format_german_float(form_surface), - format_german_float(rest_surface), - ), - ) - return is_valid_surface and objects_valid and super_result - - def __create_deduction(self): - """ Creates the deduction - - Returns: - - """ - with transaction.atomic(): - user_action_create = UserActionLogEntry.get_created_action(self.user) - deduction = EcoAccountDeduction.objects.create( - intervention=self.cleaned_data["intervention"], - account=self.cleaned_data["account"], - surface=self.cleaned_data["surface"], - created=user_action_create, - ) - return deduction - - def save(self): - deduction = self.__create_deduction() - self.cleaned_data["intervention"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED) - self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED) - return deduction - - -class EditEcoAccountDeductionModalForm(NewDeductionModalForm): - deduction = None - - def __init__(self, *args, **kwargs): - self.deduction = kwargs.pop("deduction", None) - super().__init__(*args, **kwargs) - self.form_title = _("Edit Deduction") - form_data = { - "account": self.deduction.account, - "intervention": self.deduction.intervention, - "surface": self.deduction.surface, - } - self.load_initial_data(form_data) - - def _get_available_surface(self, acc): - rest_surface = super()._get_available_surface(acc) - # Increase available surface by the currently deducted surface, so we can 'deduct' the same amount again or - # increase the surface only a little, which will still be valid. - # Example: 200 m² left, 500 m² deducted. Entering 700 m² would fail if we would not add the 500 m² to the available - # surface again. - rest_surface += self.deduction.surface - return rest_surface - - def save(self): - deduction = self.deduction - form_account = self.cleaned_data.get("account", None) - form_intervention = self.cleaned_data.get("intervention", None) - old_account = deduction.account - old_intervention = deduction.intervention - old_surface = deduction.surface - - # If account or intervention has been changed, we put that change in the logs just as if the deduction has - # been removed for this entry. Act as if the deduction is newly created for the new entries - if old_account != form_account: - old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED) - form_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED) - else: - old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED) - - if old_intervention != form_intervention: - old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED) - form_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED) - else: - old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED) - - deduction.account = form_account - deduction.intervention = self.cleaned_data.get("intervention", None) - deduction.surface = self.cleaned_data.get("surface", None) - deduction.save() - - data_changes = { - "surface": { - "old": old_surface, - "new": deduction.surface, - }, - "intervention": { - "old": old_intervention.identifier, - "new": deduction.intervention.identifier, - }, - "account": { - "old": old_account.identifier, - "new": deduction.account.identifier, - } - } - old_account.send_notification_mail_on_deduction_change(data_changes) - return deduction - - -class RemoveEcoAccountDeductionModalForm(RemoveModalForm): - """ Removing modal form for EcoAccountDeduction - - Can be used for anything, where removing shall be confirmed by the user a second time. - - """ - deduction = None - - def __init__(self, *args, **kwargs): - deduction = kwargs.pop("deduction", None) - self.deduction = deduction - super().__init__(*args, **kwargs) - - def save(self): - with transaction.atomic(): - self.deduction.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) - self.deduction.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) - self.deduction.delete() - - -class NewInterventionDocumentModalForm(NewDocumentModalForm): - document_model = InterventionDocument diff --git a/intervention/forms/modals/__init__.py b/intervention/forms/modals/__init__.py new file mode 100644 index 00000000..ca978536 --- /dev/null +++ b/intervention/forms/modals/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" diff --git a/intervention/forms/modals/check.py b/intervention/forms/modals/check.py new file mode 100644 index 00000000..8e2a551e --- /dev/null +++ b/intervention/forms/modals/check.py @@ -0,0 +1,107 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.db import transaction +from django.utils.translation import gettext_lazy as _ + +from konova.forms.modals import BaseModalForm + + +class CheckModalForm(BaseModalForm): + """ The modal form for running a check on interventions and their compensations + + """ + checked_intervention = forms.BooleanField( + label=_("Checked intervention data"), + label_suffix="", + widget=forms.CheckboxInput(), + required=True, + ) + checked_comps = forms.BooleanField( + label=_("Checked compensations data and payments"), + label_suffix="", + widget=forms.CheckboxInput(), + required=True + ) + valid = None + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Run check") + self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) + self.valid = False + + def _are_deductions_valid(self): + """ Performs validity checks on deductions and their eco-account + + Returns: + + """ + deductions = self.instance.deductions.all() + for deduction in deductions: + checker = deduction.account.quality_check() + for msg in checker.messages: + self.add_error( + "checked_comps", + f"{deduction.account.identifier}: {msg}" + ) + return checker.valid + return True + + def _are_comps_valid(self): + """ Performs validity checks on all types of compensations + + Types of compensations are + * regular Compensations + * deductions from EcoAccounts + + Returns: + + """ + comps = self.instance.compensations.filter( + deleted=None, + ) + comps_valid = True + for comp in comps: + checker = comp.quality_check() + for msg in checker.messages: + self.add_error( + "checked_comps", + f"{comp.identifier}: {msg}" + ) + comps_valid = checker.valid + deductions_valid = self._are_deductions_valid() + return deductions_valid and comps_valid + + def is_valid(self) -> bool: + """ Perform a validity check based on quality_check() logic + + Returns: + result (bool) + """ + super_valid = super().is_valid() + # Perform check + checker = self.instance.quality_check() + for msg in checker.messages: + self.add_error( + "checked_intervention", + msg + ) + all_comps_valid = self._are_comps_valid() + intervention_valid = checker.valid + + return super_valid and intervention_valid and all_comps_valid + + def save(self): + """ Saving logic + + Returns: + + """ + with transaction.atomic(): + self.instance.set_checked(self.user) diff --git a/intervention/forms/modals/deduction.py b/intervention/forms/modals/deduction.py new file mode 100644 index 00000000..2e182146 --- /dev/null +++ b/intervention/forms/modals/deduction.py @@ -0,0 +1,252 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from dal import autocomplete +from django import forms +from django.db import transaction +from django.utils.translation import gettext_lazy as _ + +from compensation.models import EcoAccount, EcoAccountDeduction +from intervention.models import Intervention +from konova.forms.modals import BaseModalForm, RemoveModalForm +from konova.utils.general import format_german_float +from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED +from user.models import UserActionLogEntry + + +class NewEcoAccountDeductionModalForm(BaseModalForm): + """ Form for creating new deduction + + Can be used for Intervention view as well as for EcoAccount views. + + Parameter 'instance' can be an intervention, as well as an ecoAccount. + An instance check handles both workflows properly. + + """ + account = forms.ModelChoiceField( + label=_("Eco-account"), + label_suffix="", + help_text=_("Only recorded accounts can be selected for deductions"), + queryset=EcoAccount.objects.filter(deleted=None), + widget=autocomplete.ModelSelect2( + url="accounts-autocomplete", + attrs={ + "data-placeholder": _("Eco-account"), + "data-minimum-input-length": 3, + "readonly": True, + } + ), + ) + surface = forms.DecimalField( + min_value=0.00, + decimal_places=2, + label=_("Surface"), + label_suffix="", + help_text=_("in m²"), + widget=forms.NumberInput( + attrs={ + "class": "form-control", + "placeholder": "0,00", + } + ) + ) + intervention = forms.ModelChoiceField( + label=_("Intervention"), + label_suffix="", + help_text=_("Only shared interventions can be selected"), + queryset=Intervention.objects.filter(deleted=None), + widget=autocomplete.ModelSelect2( + url="interventions-autocomplete", + attrs={ + "data-placeholder": _("Intervention"), + "data-minimum-input-length": 3, + } + ), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("New Deduction") + self.form_caption = _("Enter the information for a new deduction from a chosen eco-account") + + # Check for Intervention or EcoAccount + if isinstance(self.instance, Intervention): + # Form has been called with a given intervention + self.initialize_form_field("intervention", self.instance) + self.disable_form_field("intervention") + elif isinstance(self.instance, EcoAccount): + # Form has been called with a given account --> make it initial in the form and read-only + self.initialize_form_field("account", self.instance) + self.disable_form_field("account") + else: + raise NotImplementedError + + def _get_available_surface(self, acc): + """ Calculates how much available surface is left on the account + + Args: + acc (EcoAccount): + + Returns: + + """ + # Calculate valid surface + deductable_surface = acc.deductable_surface + sum_surface_deductions = acc.get_deductions_surface() + rest_surface = deductable_surface - sum_surface_deductions + return rest_surface + + def is_valid(self): + """ Custom validity check + + Makes sure the deduction can not contain more surface than the account still provides + + Returns: + is_valid (bool) + """ + super_result = super().is_valid() + acc = self.cleaned_data["account"] + intervention = self.cleaned_data["intervention"] + objects_valid = True + + if not acc.recorded: + self.add_error( + "account", + _("Eco-account {} is not recorded yet. You can only deduct from recorded accounts.").format(acc.identifier) + ) + objects_valid = False + + if intervention.is_recorded: + self.add_error( + "intervention", + _("Intervention {} is currently recorded. To change any data on it, the entry must be unrecorded.").format(intervention.identifier) + ) + objects_valid = False + + rest_surface = self._get_available_surface(acc) + form_surface = float(self.cleaned_data["surface"]) + is_valid_surface = form_surface <= rest_surface + if not is_valid_surface: + self.add_error( + "surface", + _("The account {} has not enough surface for a deduction of {} m². There are only {} m² left").format( + acc.identifier, + format_german_float(form_surface), + format_german_float(rest_surface), + ), + ) + return is_valid_surface and objects_valid and super_result + + def __create_deduction(self): + """ Creates the deduction + + Returns: + + """ + with transaction.atomic(): + user_action_create = UserActionLogEntry.get_created_action(self.user) + deduction = EcoAccountDeduction.objects.create( + intervention=self.cleaned_data["intervention"], + account=self.cleaned_data["account"], + surface=self.cleaned_data["surface"], + created=user_action_create, + ) + return deduction + + def save(self): + deduction = self.__create_deduction() + self.cleaned_data["intervention"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED) + self.cleaned_data["account"].mark_as_edited(self.user, edit_comment=DEDUCTION_ADDED) + return deduction + + +class EditEcoAccountDeductionModalForm(NewEcoAccountDeductionModalForm): + deduction = None + + def __init__(self, *args, **kwargs): + self.deduction = kwargs.pop("deduction", None) + super().__init__(*args, **kwargs) + self.form_title = _("Edit Deduction") + form_data = { + "account": self.deduction.account, + "intervention": self.deduction.intervention, + "surface": self.deduction.surface, + } + self.load_initial_data(form_data) + + def _get_available_surface(self, acc): + rest_surface = super()._get_available_surface(acc) + # Increase available surface by the currently deducted surface, so we can 'deduct' the same amount again or + # increase the surface only a little, which will still be valid. + # Example: 200 m² left, 500 m² deducted. Entering 700 m² would fail if we would not add the 500 m² to the available + # surface again. + rest_surface += self.deduction.surface + return rest_surface + + def save(self): + deduction = self.deduction + form_account = self.cleaned_data.get("account", None) + form_intervention = self.cleaned_data.get("intervention", None) + old_account = deduction.account + old_intervention = deduction.intervention + old_surface = deduction.surface + + # If account or intervention has been changed, we put that change in the logs just as if the deduction has + # been removed for this entry. Act as if the deduction is newly created for the new entries + if old_account != form_account: + old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED) + form_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED) + else: + old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED) + + if old_intervention != form_intervention: + old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED) + form_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED) + else: + old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED) + + deduction.account = form_account + deduction.intervention = self.cleaned_data.get("intervention", None) + deduction.surface = self.cleaned_data.get("surface", None) + deduction.save() + + data_changes = { + "surface": { + "old": old_surface, + "new": deduction.surface, + }, + "intervention": { + "old": old_intervention.identifier, + "new": deduction.intervention.identifier, + }, + "account": { + "old": old_account.identifier, + "new": deduction.account.identifier, + } + } + old_account.send_notification_mail_on_deduction_change(data_changes) + return deduction + + +class RemoveEcoAccountDeductionModalForm(RemoveModalForm): + """ Removing modal form for EcoAccountDeduction + + Can be used for anything, where removing shall be confirmed by the user a second time. + + """ + deduction = None + + def __init__(self, *args, **kwargs): + deduction = kwargs.pop("deduction", None) + self.deduction = deduction + super().__init__(*args, **kwargs) + + def save(self): + with transaction.atomic(): + self.deduction.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) + self.deduction.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) + self.deduction.delete() \ No newline at end of file diff --git a/intervention/forms/modals/document.py b/intervention/forms/modals/document.py new file mode 100644 index 00000000..212f3f02 --- /dev/null +++ b/intervention/forms/modals/document.py @@ -0,0 +1,13 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from intervention.models import InterventionDocument +from konova.forms.modals import NewDocumentModalForm + + +class NewInterventionDocumentModalForm(NewDocumentModalForm): + document_model = InterventionDocument \ No newline at end of file diff --git a/intervention/forms/modals/revocation.py b/intervention/forms/modals/revocation.py new file mode 100644 index 00000000..b738efe6 --- /dev/null +++ b/intervention/forms/modals/revocation.py @@ -0,0 +1,110 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.core.exceptions import ObjectDoesNotExist +from django.utils.translation import gettext_lazy as _ + +from intervention.models import RevocationDocument +from konova.forms.modals import BaseModalForm, RemoveModalForm +from konova.utils.message_templates import REVOCATION_ADDED, REVOCATION_EDITED + + +class NewRevocationModalForm(BaseModalForm): + date = forms.DateField( + label=_("Date"), + label_suffix=_(""), + help_text=_("Date of revocation"), + widget=forms.DateInput( + attrs={ + "type": "date", + "data-provide": "datepicker", + "class": "form-control", + }, + format="%d.%m.%Y" + ) + ) + file = forms.FileField( + label=_("Document"), + label_suffix=_(""), + required=False, + help_text=_("Must be smaller than 15 Mb"), + widget=forms.FileInput( + attrs={ + "class": "form-control-file" + } + ) + ) + comment = forms.CharField( + required=False, + max_length=200, + label=_("Comment"), + label_suffix=_(""), + help_text=_("Additional comment, maximum {} letters").format(200), + widget=forms.Textarea( + attrs={ + "cols": 30, + "rows": 5, + "class": "form-control", + } + ) + ) + document_model = RevocationDocument + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Add revocation") + self.form_caption = "" + self.form_attrs = { + "enctype": "multipart/form-data", # important for file upload + } + + def save(self): + revocation = self.instance.add_revocation(self) + self.instance.mark_as_edited(self.user, self.request, edit_comment=REVOCATION_ADDED) + return revocation + + +class EditRevocationModalForm(NewRevocationModalForm): + revocation = None + + def __init__(self, *args, **kwargs): + self.revocation = kwargs.pop("revocation", None) + super().__init__(*args, **kwargs) + self.form_title = _("Edit revocation") + try: + doc = self.revocation.document.file + except ObjectDoesNotExist: + doc = None + form_data = { + "date": str(self.revocation.date), + "file": doc, + "comment": self.revocation.comment, + } + self.load_initial_data(form_data) + + def save(self): + revocation = self.instance.edit_revocation(self) + self.instance.mark_as_edited(self.user, self.request, edit_comment=REVOCATION_EDITED) + return revocation + + +class RemoveRevocationModalForm(RemoveModalForm): + """ Removing modal form for Revocation + + Can be used for anything, where removing shall be confirmed by the user a second time. + + """ + revocation = None + + def __init__(self, *args, **kwargs): + revocation = kwargs.pop("revocation", None) + self.revocation = revocation + super().__init__(*args, **kwargs) + + def save(self): + self.instance.remove_revocation(self) diff --git a/intervention/forms/modals/share.py b/intervention/forms/modals/share.py new file mode 100644 index 00000000..39c9d142 --- /dev/null +++ b/intervention/forms/modals/share.py @@ -0,0 +1,136 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from dal import autocomplete +from django import forms +from django.utils.translation import gettext_lazy as _ + +from intervention.inputs import TextToClipboardInput +from konova.forms.modals import BaseModalForm +from konova.utils.message_templates import ENTRY_REMOVE_MISSING_PERMISSION +from konova.utils.user_checks import is_default_group_only +from user.models import Team, User + + +class ShareModalForm(BaseModalForm): + url = forms.CharField( + label=_("Share link"), + label_suffix="", + help_text=_("Send this link to users who you want to have writing access on the data"), + required=False, + widget=TextToClipboardInput( + attrs={ + "readonly": True, + "class": "form-control", + } + ) + ) + teams = forms.ModelMultipleChoiceField( + label=_("Add team to share with"), + label_suffix="", + help_text=_("Multiple selection possible - You can only select teams which do not already have access."), + required=False, + queryset=Team.objects.all(), + widget=autocomplete.ModelSelect2Multiple( + url="share-team-autocomplete", + attrs={ + "data-placeholder": _("Click for selection"), + "data-minimum-input-length": 3, + }, + ), + ) + users = forms.ModelMultipleChoiceField( + label=_("Add user to share with"), + label_suffix="", + help_text=_("Multiple selection possible - You can only select users which do not already have access. Enter the full username."), + required=False, + queryset=User.objects.all(), + widget=autocomplete.ModelSelect2Multiple( + url="share-user-autocomplete", + attrs={ + "data-placeholder": _("Click for selection"), + "data-minimum-input-length": 3, + }, + ), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Share") + self.form_caption = _("Share settings for {}").format(self.instance.identifier) + self.template = "modal/modal_form.html" + + # Make sure an access_token is set + if self.instance.access_token is None: + self.instance.generate_access_token() + + self._init_fields() + + def _user_team_valid(self): + """ Checks whether users and teams have been removed by the user and if the user is allowed to do so or not + + Returns: + + """ + users = self.cleaned_data.get("users", User.objects.none()) + teams = self.cleaned_data.get("teams", Team.objects.none()) + + _is_valid = True + if is_default_group_only(self.user): + shared_users = self.instance.shared_users + shared_teams = self.instance.shared_teams + + shared_users_are_removed = not set(shared_users).issubset(users) + shared_teams_are_removed = not set(shared_teams).issubset(teams) + + if shared_users_are_removed: + self.add_error( + "users", + ENTRY_REMOVE_MISSING_PERMISSION + ) + _is_valid = False + if shared_teams_are_removed: + self.add_error( + "teams", + ENTRY_REMOVE_MISSING_PERMISSION + ) + _is_valid = False + return _is_valid + + def is_valid(self): + """ Extended validity check + + Returns: + + """ + super_valid = super().is_valid() + user_team_valid = self._user_team_valid() + _is_valid = super_valid and user_team_valid + return _is_valid + + def _init_fields(self): + """ Wraps initializing of fields + + Returns: + + """ + # Initialize share_link field + share_link = self.instance.get_share_link() + self.share_link = self.request.build_absolute_uri(share_link) + self.initialize_form_field( + "url", + self.share_link + ) + + form_data = { + "teams": self.instance.teams.all(), + "users": self.instance.users.all(), + } + self.load_initial_data(form_data) + + def save(self): + self.instance.update_shared_access(self) diff --git a/intervention/views.py b/intervention/views.py index 6a9304e9..e622488f 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -4,10 +4,14 @@ from django.utils.translation import gettext_lazy as _ from django.http import HttpRequest, JsonResponse, Http404 from django.shortcuts import render -from intervention.forms.forms import NewInterventionForm, EditInterventionForm -from intervention.forms.modalForms import ShareModalForm, NewRevocationModalForm, \ - CheckModalForm, NewDeductionModalForm, NewInterventionDocumentModalForm, RemoveEcoAccountDeductionModalForm, \ - RemoveRevocationModalForm, EditEcoAccountDeductionModalForm, EditRevocationModalForm +from intervention.forms.intervention import NewInterventionForm, EditInterventionForm +from intervention.forms.modals.check import CheckModalForm +from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, RemoveEcoAccountDeductionModalForm, \ + EditEcoAccountDeductionModalForm +from intervention.forms.modals.document import NewInterventionDocumentModalForm +from intervention.forms.modals.revocation import EditRevocationModalForm, RemoveRevocationModalForm, \ + NewRevocationModalForm +from intervention.forms.modals.share import ShareModalForm from intervention.models import Intervention, Revocation, InterventionDocument, RevocationDocument from intervention.tables import InterventionTable from konova.contexts import BaseContext @@ -583,7 +587,7 @@ def new_deduction_view(request: HttpRequest, id: str): """ intervention = get_object_or_404(Intervention, id=id) - form = NewDeductionModalForm(request.POST or None, instance=intervention, request=request) + form = NewEcoAccountDeductionModalForm(request.POST or None, instance=intervention, request=request) return form.process_request( request, msg_success=DEDUCTION_ADDED, diff --git a/konova/forms/base_form.py b/konova/forms/base_form.py index 065fba17..fb69999c 100644 --- a/konova/forms/base_form.py +++ b/konova/forms/base_form.py @@ -134,7 +134,7 @@ class BaseForm(forms.Form): Returns: """ - from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \ + from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \ RemoveEcoAccountDeductionModalForm from konova.forms.modals.resubmission_form import ResubmissionModalForm is_none = self.instance is None @@ -142,7 +142,7 @@ class BaseForm(forms.Form): is_deduction_form_from_account = isinstance( self, ( - NewDeductionModalForm, + NewEcoAccountDeductionModalForm, ResubmissionModalForm, EditEcoAccountDeductionModalForm, RemoveEcoAccountDeductionModalForm, From 05c1bf677c99edb0963e360874ec048e7a150342 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 18 Aug 2022 10:17:09 +0200 Subject: [PATCH 13/48] User forms * refactors user/forms.py by splitting into modals package and regular forms * regular forms can now be found at user/forms/user.py and user/forms/team.py * modal forms can now be found at user/forms/modals/... --- user/forms/__init__.py | 7 + user/forms/modals/__init__.py | 7 + user/{forms.py => forms/modals/team.py} | 200 +----------------------- user/forms/modals/user.py | 62 ++++++++ user/forms/team.py | 54 +++++++ user/forms/user.py | 113 +++++++++++++ user/views.py | 6 +- 7 files changed, 251 insertions(+), 198 deletions(-) create mode 100644 user/forms/__init__.py create mode 100644 user/forms/modals/__init__.py rename user/{forms.py => forms/modals/team.py} (52%) create mode 100644 user/forms/modals/user.py create mode 100644 user/forms/team.py create mode 100644 user/forms/user.py diff --git a/user/forms/__init__.py b/user/forms/__init__.py new file mode 100644 index 00000000..ca978536 --- /dev/null +++ b/user/forms/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" diff --git a/user/forms/modals/__init__.py b/user/forms/modals/__init__.py new file mode 100644 index 00000000..ca978536 --- /dev/null +++ b/user/forms/modals/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" diff --git a/user/forms.py b/user/forms/modals/team.py similarity index 52% rename from user/forms.py rename to user/forms/modals/team.py index 0649eaf3..d63a2306 100644 --- a/user/forms.py +++ b/user/forms/modals/team.py @@ -1,166 +1,17 @@ """ Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 08.07.21 +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 """ from dal import autocomplete from django import forms from django.db import transaction -from django.urls import reverse, reverse_lazy +from django.urls import reverse from django.utils.translation import gettext_lazy as _ - -from api.models import APIUserToken -from intervention.inputs import GenerateInput -from user.models import User, UserNotification, Team - from konova.forms.modals import BaseModalForm, RemoveModalForm -from konova.forms import BaseForm - - -class UserNotificationForm(BaseForm): - """ Form for changing the notification settings of a user - - """ - notifications = forms.MultipleChoiceField( - label_suffix="", - label=_("Notifications"), - required=False, # allow total disabling of all notifications - help_text=_("Select the situations when you want to receive a notification"), - widget=forms.CheckboxSelectMultiple( - attrs={ - "class": "list-unstyled", - } - ), - choices=[] - ) - - def __init__(self, user: User, *args, **kwargs): - super().__init__(*args, **kwargs) - self.user = user - self.form_title = _("Edit notifications") - self.form_caption = _("") - self.action_url = reverse("user:notifications") - self.cancel_redirect = reverse("user:index") - - # Insert all notifications into form field by creating choices as tuples - notifications = UserNotification.objects.filter( - is_active=True, - ) - choices = [] - for n in notifications: - choices.append( - (n.id, _(n.name)) - ) - self.fields["notifications"].choices = choices - - users_current_notifications = self.user.notifications.all() - users_current_notifications = [str(n.id) for n in users_current_notifications] - self.fields["notifications"].initial = users_current_notifications - - def save(self): - """ Stores the changes in the user konova_extension - - Returns: - - """ - selected_notification_ids = self.cleaned_data.get("notifications", []) - notifications = UserNotification.objects.filter( - id__in=selected_notification_ids, - ) - self.user.notifications.set(notifications) - - -class UserContactForm(BaseModalForm): - name = forms.CharField( - label=_("Username"), - label_suffix="", - required=False, - widget=forms.TextInput( - attrs={ - "readonly": True, - "class": "form-control", - } - ), - ) - person_name = forms.CharField( - label=_("Person name"), - label_suffix="", - required=False, - widget=forms.TextInput( - attrs={ - "readonly": True, - "class": "form-control", - } - ), - ) - mail = forms.EmailField( - label=_("E-Mail"), - label_suffix="", - required=False, - widget=forms.TextInput( - attrs={ - "readonly": True, - "class": "form-control", - } - ), - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.render_submit = False - self.form_title = _("User contact data") - self.form_caption = "" - - self.initialize_form_field("name", self.instance.username) - self.initialize_form_field("person_name", "{} {}".format(self.instance.first_name, self.instance.last_name)) - self.initialize_form_field("mail", self.instance.email) - - -class UserAPITokenForm(BaseForm): - token = forms.CharField( - label=_("Token"), - label_suffix="", - max_length=255, - required=True, - help_text=_("Generated automatically"), - widget=GenerateInput( - attrs={ - "class": "form-control", - "url": reverse_lazy("api:generate-new-token"), - } - ) - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Create new token") - self.form_caption = _("A new token needs to be validated by an administrator!") - - self.action_url = reverse("user:api-token") - self.cancel_redirect = reverse("user:index") - - # Make direct token editing by user impossible. Instead set the proper url for generating a new token - self.initialize_form_field("token", None) - self.fields["token"].widget.attrs["readonly"] = True - - def save(self): - """ Saves the form data - - Returns: - api_token (APIUserToken) - """ - user = self.instance - new_token = self.cleaned_data["token"] - if user.api_token is not None: - user.api_token.delete() - new_token = APIUserToken.objects.create( - token=new_token - ) - user.api_token = new_token - user.save() - return new_token +from user.models import User, Team class NewTeamModalForm(BaseModalForm): @@ -347,46 +198,3 @@ class LeaveTeamModalForm(RemoveModalForm): def save(self): self.instance.remove_user(self.user) - - -class TeamDataForm(BaseModalForm): - name = forms.CharField( - label_suffix="", - label=_("Team name"), - max_length=500, - required=False, - widget=forms.TextInput( - attrs={ - "placeholder": _("Team name"), - "class": "form-control", - } - ) - ) - description = forms.CharField( - label_suffix="", - required=False, - label=_("Description"), - widget=forms.Textarea( - attrs={ - "rows": 5, - "class": "form-control" - } - ) - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Team") - self.form_caption = "" - self.render_submit = False - form_data = { - "name": self.instance.name, - "description": self.instance.description, - } - self.load_initial_data( - form_data, - [ - "name", - "description" - ] - ) \ No newline at end of file diff --git a/user/forms/modals/user.py b/user/forms/modals/user.py new file mode 100644 index 00000000..d5631050 --- /dev/null +++ b/user/forms/modals/user.py @@ -0,0 +1,62 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.utils.translation import gettext_lazy as _ + +from konova.forms.modals import BaseModalForm + + +class UserContactForm(BaseModalForm): + def save(self): + # Readonly form. No saving needed + pass + + name = forms.CharField( + label=_("Username"), + label_suffix="", + required=False, + widget=forms.TextInput( + attrs={ + "readonly": True, + "class": "form-control", + } + ), + ) + person_name = forms.CharField( + label=_("Person name"), + label_suffix="", + required=False, + widget=forms.TextInput( + attrs={ + "readonly": True, + "class": "form-control", + } + ), + ) + mail = forms.EmailField( + label=_("E-Mail"), + label_suffix="", + required=False, + widget=forms.TextInput( + attrs={ + "readonly": True, + "class": "form-control", + } + ), + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.render_submit = False + self.form_title = _("User contact data") + self.form_caption = "" + + self.initialize_form_field("name", self.instance.username) + self.initialize_form_field("person_name", "{} {}".format(self.instance.first_name, self.instance.last_name)) + self.initialize_form_field("mail", self.instance.email) + diff --git a/user/forms/team.py b/user/forms/team.py new file mode 100644 index 00000000..dab89842 --- /dev/null +++ b/user/forms/team.py @@ -0,0 +1,54 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.utils.translation import gettext_lazy as _ + +from konova.forms.modals import BaseModalForm + + +class TeamDataForm(BaseModalForm): + name = forms.CharField( + label_suffix="", + label=_("Team name"), + max_length=500, + required=False, + widget=forms.TextInput( + attrs={ + "placeholder": _("Team name"), + "class": "form-control", + } + ) + ) + description = forms.CharField( + label_suffix="", + required=False, + label=_("Description"), + widget=forms.Textarea( + attrs={ + "rows": 5, + "class": "form-control" + } + ) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Team") + self.form_caption = "" + self.render_submit = False + form_data = { + "name": self.instance.name, + "description": self.instance.description, + } + self.load_initial_data( + form_data, + [ + "name", + "description" + ] + ) \ No newline at end of file diff --git a/user/forms/user.py b/user/forms/user.py new file mode 100644 index 00000000..99afc372 --- /dev/null +++ b/user/forms/user.py @@ -0,0 +1,113 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.urls import reverse, reverse_lazy +from django.utils.translation import gettext_lazy as _ + +from api.models import APIUserToken +from intervention.inputs import GenerateInput +from konova.forms import BaseForm +from user.models import User, UserNotification + + +class UserNotificationForm(BaseForm): + """ Form for changing the notification settings of a user + + """ + notifications = forms.MultipleChoiceField( + label_suffix="", + label=_("Notifications"), + required=False, # allow total disabling of all notifications + help_text=_("Select the situations when you want to receive a notification"), + widget=forms.CheckboxSelectMultiple( + attrs={ + "class": "list-unstyled", + } + ), + choices=[] + ) + + def __init__(self, user: User, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = user + self.form_title = _("Edit notifications") + self.form_caption = _("") + self.action_url = reverse("user:notifications") + self.cancel_redirect = reverse("user:index") + + # Insert all notifications into form field by creating choices as tuples + notifications = UserNotification.objects.filter( + is_active=True, + ) + choices = [] + for n in notifications: + choices.append( + (n.id, _(n.name)) + ) + self.fields["notifications"].choices = choices + + users_current_notifications = self.user.notifications.all() + users_current_notifications = [str(n.id) for n in users_current_notifications] + self.fields["notifications"].initial = users_current_notifications + + def save(self): + """ Stores the changes in the user konova_extension + + Returns: + + """ + selected_notification_ids = self.cleaned_data.get("notifications", []) + notifications = UserNotification.objects.filter( + id__in=selected_notification_ids, + ) + self.user.notifications.set(notifications) + + +class UserAPITokenForm(BaseForm): + token = forms.CharField( + label=_("Token"), + label_suffix="", + max_length=255, + required=True, + help_text=_("Generated automatically"), + widget=GenerateInput( + attrs={ + "class": "form-control", + "url": reverse_lazy("api:generate-new-token"), + } + ) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Create new token") + self.form_caption = _("A new token needs to be validated by an administrator!") + + self.action_url = reverse("user:api-token") + self.cancel_redirect = reverse("user:index") + + # Make direct token editing by user impossible. Instead set the proper url for generating a new token + self.initialize_form_field("token", None) + self.fields["token"].widget.attrs["readonly"] = True + + def save(self): + """ Saves the form data + + Returns: + api_token (APIUserToken) + """ + user = self.instance + new_token = self.cleaned_data["token"] + if user.api_token is not None: + user.api_token.delete() + new_token = APIUserToken.objects.create( + token=new_token + ) + user.api_token = new_token + user.save() + return new_token \ No newline at end of file diff --git a/user/views.py b/user/views.py index 2c5b86a1..7e2d21e6 100644 --- a/user/views.py +++ b/user/views.py @@ -5,6 +5,10 @@ from django.urls import reverse from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.mailer import Mailer from konova.utils.message_templates import FORM_INVALID +from user.forms.modals.team import NewTeamModalForm, EditTeamModalForm, RemoveTeamModalForm, LeaveTeamModalForm +from user.forms.modals.user import UserContactForm +from user.forms.team import TeamDataForm +from user.forms.user import UserNotificationForm, UserAPITokenForm from user.models import User, Team from django.http import HttpRequest, Http404 from django.shortcuts import render, redirect, get_object_or_404 @@ -12,8 +16,6 @@ from django.utils.translation import gettext_lazy as _ from konova.contexts import BaseContext from konova.decorators import any_group_check, default_group_required -from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm, NewTeamModalForm, EditTeamModalForm, \ - RemoveTeamModalForm, TeamDataForm, LeaveTeamModalForm @login_required From ca6770ec54af6c50f461df87234d3c6c30de16ef Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 18 Aug 2022 10:24:11 +0200 Subject: [PATCH 14/48] Tables refactoring * splits compensation tables.py into individual files for compensation table and eco account table --- compensation/tables/__init__.py | 7 + .../{tables.py => tables/compensation.py} | 167 +---------------- compensation/tables/eco_account.py | 174 ++++++++++++++++++ compensation/views/compensation.py | 2 +- compensation/views/eco_account.py | 2 +- 5 files changed, 188 insertions(+), 164 deletions(-) create mode 100644 compensation/tables/__init__.py rename compensation/{tables.py => tables/compensation.py} (54%) create mode 100644 compensation/tables/eco_account.py diff --git a/compensation/tables/__init__.py b/compensation/tables/__init__.py new file mode 100644 index 00000000..ca978536 --- /dev/null +++ b/compensation/tables/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" diff --git a/compensation/tables.py b/compensation/tables/compensation.py similarity index 54% rename from compensation/tables.py rename to compensation/tables/compensation.py index 1373f2cd..92fe1840 100644 --- a/compensation/tables.py +++ b/compensation/tables/compensation.py @@ -1,19 +1,19 @@ """ Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 01.12.20 +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 """ -from konova.utils.message_templates import DATA_IS_UNCHECKED, DATA_CHECKED_ON_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE from django.http import HttpRequest from django.template.loader import render_to_string from django.urls import reverse from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ -from compensation.filters import CompensationTableFilter, EcoAccountTableFilter -from compensation.models import Compensation, EcoAccount +from compensation.filters import CompensationTableFilter +from compensation.models import Compensation +from konova.utils.message_templates import DATA_IS_UNCHECKED, DATA_CHECKED_ON_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE from konova.utils.tables import BaseTable, TableRenderMixin import django_tables2 as tables @@ -187,160 +187,3 @@ class CompensationTable(BaseTable, TableRenderMixin): icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit", ) return format_html(html) - - -class EcoAccountTable(BaseTable, TableRenderMixin): - id = tables.Column( - verbose_name=_("Identifier"), - orderable=True, - accessor="identifier", - ) - t = tables.Column( - verbose_name=_("Title"), - orderable=True, - accessor="title", - ) - d = tables.Column( - verbose_name=_("Parcel gmrkng"), - orderable=True, - accessor="geometry", - ) - av = tables.Column( - verbose_name=_("Available"), - orderable=True, - empty_values=[], - attrs={ - "th": { - "class": "w-20", - } - } - ) - r = tables.Column( - verbose_name=_("Recorded"), - orderable=True, - empty_values=[], - accessor="recorded", - ) - e = tables.Column( - verbose_name=_("Editable"), - orderable=True, - empty_values=[], - accessor="users", - ) - lm = tables.Column( - verbose_name=_("Last edit"), - orderable=True, - accessor="modified__timestamp", - ) - - class Meta(BaseTable.Meta): - template_name = "django_tables2/bootstrap4.html" - - def __init__(self, request: HttpRequest, *args, **kwargs): - self.title = _("Eco Accounts") - self.add_new_url = reverse("compensation:acc:new") - qs = kwargs.get("queryset", None) - self.filter = EcoAccountTableFilter( - user=request.user, - data=request.GET, - queryset=qs, - ) - kwargs["queryset"] = self.filter.qs - super().__init__(request, *args, **kwargs) - - def render_id(self, value, record: EcoAccount): - """ Renders the id column for an eco account - - Args: - value (str): The identifier value - record (EcoAccount): The eco account record - - Returns: - - """ - html = "" - html += self.render_link( - tooltip=_("Open {}").format(_("Eco-account")), - href=reverse("compensation:acc:detail", args=(record.id,)), - txt=value, - new_tab=False, - ) - return format_html(html) - - def render_av(self, value, record: EcoAccount): - """ Renders the available column for an eco account - - Args: - value (str): The identifier value - record (EcoAccount): The eco account record - - Returns: - - """ - value_total, value_relative = record.get_available_rest() - html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative}) - return format_html(html) - - def render_d(self, value, record: Compensation): - """ Renders the parcel district column for a compensation - - Args: - value (str): The geometry - record (Compensation): The compensation record - - Returns: - - """ - parcels = value.get_underlying_parcels().values_list( - "parcel_group__name", - flat=True - ).distinct() - html = render_to_string( - "table/gmrkng_col.html", - { - "entries": parcels - } - ) - return html - - def render_r(self, value, record: EcoAccount): - """ Renders the recorded column for an eco account - - Args: - value (str): The identifier value - record (EcoAccount): The eco account record - - Returns: - - """ - html = "" - checked = value is not None - tooltip = _("Not recorded yet. Can not be used for deductions, yet.") - if checked: - on = value.get_timestamp_str_formatted() - tooltip = _("Recorded on {} by {}").format(on, record.recorded.user) - html += self.render_bookmark( - tooltip=tooltip, - icn_filled=checked, - ) - return format_html(html) - - def render_e(self, value, record: EcoAccount): - """ Renders the editable column for an eco account - - Args: - value (str): The identifier value - record (EcoAccount): The eco account record - - Returns: - - """ - html = "" - # Do not use value in here, since value does use unprefetched 'users' manager, where record has already - # prefetched users data - has_access = record.is_shared_with(self.user) - html += self.render_icn( - tooltip=_("Full access granted") if has_access else _("Access not granted"), - icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit", - ) - return format_html(html) diff --git a/compensation/tables/eco_account.py b/compensation/tables/eco_account.py new file mode 100644 index 00000000..0cf4a4ef --- /dev/null +++ b/compensation/tables/eco_account.py @@ -0,0 +1,174 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django.http import HttpRequest +from django.template.loader import render_to_string +from django.urls import reverse +from django.utils.html import format_html +from django.utils.translation import gettext_lazy as _ + +from compensation.filters import EcoAccountTableFilter +from compensation.models import EcoAccount +from konova.utils.tables import TableRenderMixin, BaseTable + +import django_tables2 as tables + +class EcoAccountTable(BaseTable, TableRenderMixin): + id = tables.Column( + verbose_name=_("Identifier"), + orderable=True, + accessor="identifier", + ) + t = tables.Column( + verbose_name=_("Title"), + orderable=True, + accessor="title", + ) + d = tables.Column( + verbose_name=_("Parcel gmrkng"), + orderable=True, + accessor="geometry", + ) + av = tables.Column( + verbose_name=_("Available"), + orderable=True, + empty_values=[], + attrs={ + "th": { + "class": "w-20", + } + } + ) + r = tables.Column( + verbose_name=_("Recorded"), + orderable=True, + empty_values=[], + accessor="recorded", + ) + e = tables.Column( + verbose_name=_("Editable"), + orderable=True, + empty_values=[], + accessor="users", + ) + lm = tables.Column( + verbose_name=_("Last edit"), + orderable=True, + accessor="modified__timestamp", + ) + + class Meta(BaseTable.Meta): + template_name = "django_tables2/bootstrap4.html" + + def __init__(self, request: HttpRequest, *args, **kwargs): + self.title = _("Eco Accounts") + self.add_new_url = reverse("compensation:acc:new") + qs = kwargs.get("queryset", None) + self.filter = EcoAccountTableFilter( + user=request.user, + data=request.GET, + queryset=qs, + ) + kwargs["queryset"] = self.filter.qs + super().__init__(request, *args, **kwargs) + + def render_id(self, value, record: EcoAccount): + """ Renders the id column for an eco account + + Args: + value (str): The identifier value + record (EcoAccount): The eco account record + + Returns: + + """ + html = "" + html += self.render_link( + tooltip=_("Open {}").format(_("Eco-account")), + href=reverse("compensation:acc:detail", args=(record.id,)), + txt=value, + new_tab=False, + ) + return format_html(html) + + def render_av(self, value, record: EcoAccount): + """ Renders the available column for an eco account + + Args: + value (str): The identifier value + record (EcoAccount): The eco account record + + Returns: + + """ + value_total, value_relative = record.get_available_rest() + html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative}) + return format_html(html) + + def render_d(self, value, record): + """ Renders the parcel district column for a compensation + + Args: + value (str): The geometry + record (Compensation): The compensation record + + Returns: + + """ + parcels = value.get_underlying_parcels().values_list( + "parcel_group__name", + flat=True + ).distinct() + html = render_to_string( + "table/gmrkng_col.html", + { + "entries": parcels + } + ) + return html + + def render_r(self, value, record: EcoAccount): + """ Renders the recorded column for an eco account + + Args: + value (str): The identifier value + record (EcoAccount): The eco account record + + Returns: + + """ + html = "" + checked = value is not None + tooltip = _("Not recorded yet. Can not be used for deductions, yet.") + if checked: + on = value.get_timestamp_str_formatted() + tooltip = _("Recorded on {} by {}").format(on, record.recorded.user) + html += self.render_bookmark( + tooltip=tooltip, + icn_filled=checked, + ) + return format_html(html) + + def render_e(self, value, record: EcoAccount): + """ Renders the editable column for an eco account + + Args: + value (str): The identifier value + record (EcoAccount): The eco account record + + Returns: + + """ + html = "" + # Do not use value in here, since value does use unprefetched 'users' manager, where record has already + # prefetched users data + has_access = record.is_shared_with(self.user) + html += self.render_icn( + tooltip=_("Full access granted") if has_access else _("Access not granted"), + icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit", + ) + return format_html(html) diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py index e12324ce..c1bec047 100644 --- a/compensation/views/compensation.py +++ b/compensation/views/compensation.py @@ -12,7 +12,7 @@ from compensation.forms.modals.compensation_action import NewCompensationActionM EditCompensationActionModalForm, RemoveCompensationActionModalForm from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm from compensation.models import Compensation, CompensationState, CompensationAction, CompensationDocument -from compensation.tables import CompensationTable +from compensation.tables.compensation import CompensationTable from intervention.models import Intervention from konova.contexts import BaseContext from konova.decorators import * diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index 07796d14..51aa925d 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -22,7 +22,7 @@ from compensation.forms.modals.document import NewEcoAccountDocumentModalForm from compensation.forms.modals.state import NewCompensationStateModalForm, RemoveCompensationStateModalForm, \ EditCompensationStateModalForm from compensation.models import EcoAccount, EcoAccountDocument, CompensationState, CompensationAction -from compensation.tables import EcoAccountTable +from compensation.tables.eco_account import EcoAccountTable from intervention.forms.modals.deduction import RemoveEcoAccountDeductionModalForm, NewEcoAccountDeductionModalForm, \ EditEcoAccountDeductionModalForm from intervention.forms.modals.share import ShareModalForm From c07933a9bf181f33aa608ed00de649f9ea68e9d7 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 18 Aug 2022 10:45:15 +0200 Subject: [PATCH 15/48] Konova filter mixins * refactors konova/mixins.py into individual files in konova/mixins/... --- compensation/filters.py | 2 +- konova/filters/mixins.py | 414 ------------------------- konova/filters/mixins/__init__.py | 7 + konova/filters/mixins/file_number.py | 33 ++ konova/filters/mixins/geo_reference.py | 209 +++++++++++++ konova/filters/mixins/keyword.py | 41 +++ konova/filters/mixins/office.py | 77 +++++ konova/filters/mixins/record.py | 51 +++ konova/filters/mixins/share.py | 57 ++++ konova/filters/table_filters.py | 9 +- 10 files changed, 482 insertions(+), 418 deletions(-) delete mode 100644 konova/filters/mixins.py create mode 100644 konova/filters/mixins/__init__.py create mode 100644 konova/filters/mixins/file_number.py create mode 100644 konova/filters/mixins/geo_reference.py create mode 100644 konova/filters/mixins/keyword.py create mode 100644 konova/filters/mixins/office.py create mode 100644 konova/filters/mixins/record.py create mode 100644 konova/filters/mixins/share.py diff --git a/compensation/filters.py b/compensation/filters.py index c9bf9b70..186a7214 100644 --- a/compensation/filters.py +++ b/compensation/filters.py @@ -10,7 +10,7 @@ from django.utils.translation import gettext_lazy as _ from django import forms from django.db.models import QuerySet, Q -from konova.filters.mixins import ConservationOfficeTableFilterMixin +from konova.filters.mixins.office import ConservationOfficeTableFilterMixin from konova.filters.table_filters import QueryTableFilter, CheckboxTableFilter, SelectionTableFilter, AbstractTableFilter diff --git a/konova/filters/mixins.py b/konova/filters/mixins.py deleted file mode 100644 index cfd9cc4c..00000000 --- a/konova/filters/mixins.py +++ /dev/null @@ -1,414 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 12.01.22 - -""" -import django_filters -from django import forms -from django.db.models import QuerySet, Q -from django.utils.translation import gettext_lazy as _ -from dal_select2.widgets import ModelSelect2 - -from codelist.models import KonovaCode -from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID -from intervention.inputs import DummyFilterInput -from konova.models import Parcel, District - - -class KeywordTableFilterMixin(django_filters.FilterSet): - q = django_filters.Filter( - method='filter_by_keyword', - # Since we use a custom search bar in the template, we need to 'render' this filter - # as 'anonymous' HiddenInput (no id, no name). This way our custom search bar's id and name won't be - # overwritten with these id and name (which would be equal) - # This way we can use the simple filter method mapping for a parameter without using a predefined widget! - widget=DummyFilterInput(), - ) - - class Meta: - abstract = True - - def filter_by_keyword(self, queryset, name, value) -> QuerySet: - """ Filters queryset depending on value of search bar input - - Args: - queryset (): - name (): - value (): - - Returns: - - """ - value = value.strip() - # build filter expression - q = Q(title__icontains=value) | Q(identifier__icontains=value) - return queryset.filter(q) - - -class FileNumberTableFilterMixin(django_filters.FilterSet): - rf = django_filters.CharFilter( - method="filter_file_number", - label=_(""), - label_suffix=_(""), - widget=forms.TextInput( - attrs={ - "placeholder": _("File number"), - "title": _("Search for file number"), - "class": "form-control", - } - ), - ) - - def filter_file_number(self, queryset, name, value) -> QuerySet: - queryset = queryset.filter( - Q(responsible__registration_file_number__icontains=value) | - Q(responsible__conservation_file_number__icontains=value) - ) - return queryset - - -class GeoReferencedTableFilterMixin(django_filters.FilterSet): - """ A mixin for AbstractTableFilter - - Specialized on filtering GeoReferenced model types - - """ - # Parcel gmrkng - di = django_filters.CharFilter( - method="filter_district", - label=_(""), - label_suffix=_(""), - widget=forms.TextInput( - attrs={ - "placeholder": _("District"), - "title": _("Search for district"), - "class": "form-control", - } - ), - ) - # Parcel gmrkng - pg = django_filters.CharFilter( - method="filter_gmrkng", - label=_(""), - label_suffix=_(""), - widget=forms.TextInput( - attrs={ - "placeholder": _("Parcel gmrkng"), - "title": _("Search for parcel gmrkng"), - "class": "form-control", - } - ), - ) - # Parcel - p = django_filters.CharFilter( - method="filter_parcel", - label=_(""), - label_suffix=_(""), - widget=forms.TextInput( - attrs={ - "placeholder": _("Parcel"), - "title": _("Search for parcel"), - "class": "form-control", - } - ), - ) - # Parcel counter - pc = django_filters.CharFilter( - method="filter_parcel_counter", - label=_(""), - label_suffix=_(""), - widget=forms.TextInput( - attrs={ - "placeholder": _("Parcel counter"), - "title": _("Search for parcel counter"), - "class": "form-control", - } - ), - ) - - # Parcel counter - pn = django_filters.CharFilter( - method="filter_parcel_number", - label=_(""), - label_suffix=_(""), - widget=forms.TextInput( - attrs={ - "placeholder": _("Parcel number"), - "title": _("Search for parcel number"), - "class": "form-control", - } - ), - ) - - class Meta: - abstract = True - - def _filter_parcel_reference(self, queryset, filter_q) -> QuerySet: - """ Filters the parcel entries by a given filter_q - - Args: - queryset (QuerySet): The queryset - filter_q (Q): The Q-style filter expression - - Returns: - - """ - matching_parcels = Parcel.objects.filter( - filter_q - ) - - related_geoms = matching_parcels.values( - "geometries" - ).distinct() - queryset = queryset.filter( - geometry__id__in=related_geoms - ) - return queryset - - def filter_district(self, queryset, name, value) -> QuerySet: - """ Filters queryset depending on value for 'Gemarkung' - - Args: - queryset (): - name (): - value (): - - Returns: - - """ - matching_districts = District.objects.filter( - Q(name__icontains=value) | - Q(key__icontains=value) - ).distinct() - matching_parcels = Parcel.objects.filter( - district__in=matching_districts - ) - related_geoms = matching_parcels.values( - "geometries" - ).distinct() - queryset = queryset.filter( - geometry__id__in=related_geoms - ) - return queryset - - def filter_gmrkng(self, queryset, name, value) -> QuerySet: - """ Filters queryset depending on value for 'Gemarkung' - - Args: - queryset (): - name (): - value (): - - Returns: - - """ - queryset = self._filter_parcel_reference( - queryset, - Q(parcel_group__name__icontains=value) | Q(parcel_group__key__icontains=value), - ) - return queryset - - def filter_parcel(self, queryset, name, value) -> QuerySet: - """ Filters queryset depending on value for 'Parcel' - - Args: - queryset (): - name (): - value (): - - Returns: - - """ - value = value.replace("-", "") - queryset = self._filter_parcel_reference( - queryset, - Q(flr=value), - ) - return queryset - - def filter_parcel_counter(self, queryset, name, value) -> QuerySet: - """ Filters queryset depending on value for 'Parcel' - - Args: - queryset (): - name (): - value (): - - Returns: - - """ - value = value.replace("-", "") - queryset = self._filter_parcel_reference( - queryset, - Q(flrstck_zhlr=value) - ) - return queryset - - def filter_parcel_number(self, queryset, name, value) -> QuerySet: - """ Filters queryset depending on value for 'Parcel' - - Args: - queryset (): - name (): - value (): - - Returns: - - """ - value = value.replace("-", "") - queryset = self._filter_parcel_reference( - queryset, - Q(flrstck_nnr=value), - ) - return queryset - - -class ShareableTableFilterMixin(django_filters.FilterSet): - """ A mixin for AbstractTableFilter - - Specialized on filtering shareable model types - - """ - sa = django_filters.BooleanFilter( - method='filter_show_all', - label=_("Show unshared"), - label_suffix=_(""), - widget=forms.CheckboxInput( - attrs={ - "class": "form-check-input", - } - ) - ) - - class Meta: - abstract = True - - def __init__(self, *args, **kwargs): - self.user = kwargs.pop("user", None) - if self.user is None: - raise AttributeError("User must be set for further filtering!") - super().__init__(*args, **kwargs) - - def filter_show_all(self, queryset, name, value) -> QuerySet: - """ Filters queryset depending on value of 'show_all' setting - - Args: - queryset (): - name (): - value (): - - Returns: - - """ - if not value: - return queryset.filter( - Q(users__in=[self.user]) | # requesting user has access - Q(teams__in=self.user.shared_teams) - ).distinct() - else: - return queryset - - -class RecordableTableFilterMixin(django_filters.FilterSet): - """ A mixin for AbstractTableFilter - - Specialized on filtering recordable model types - - """ - sr = django_filters.BooleanFilter( - method='filter_show_recorded', - label=_("Show recorded"), - label_suffix=_(""), - widget=forms.CheckboxInput( - attrs={ - "class": "form-check-input", - } - ) - ) - - class Meta: - abstract = True - - def filter_show_recorded(self, queryset, name, value) -> QuerySet: - """ Filters queryset depending on value of 'show_recorded' setting - - Args: - queryset (): - name (): - value (): - - Returns: - - """ - if not value: - return queryset.filter( - recorded=None, - ) - else: - return queryset - - -class RegistrationOfficeTableFilterMixin(django_filters.FilterSet): - """ A mixin for AbstractTableFilter - - Specialized on filtering for related registration offices - - """ - ro = django_filters.ModelChoiceFilter( - method="filter_reg_office", - label=_(""), - label_suffix=_(""), - queryset=KonovaCode.objects.filter( - is_archived=False, - is_leaf=True, - code_lists__in=[CODELIST_REGISTRATION_OFFICE_ID], - ), - widget=ModelSelect2( - url="codes-registration-office-autocomplete", - attrs={ - "data-placeholder": _("Registration office"), - "title": _("Search for registration office"), - "class": "", - } - ), - ) - - def filter_reg_office(self, queryset, name, value): - qs = queryset.filter( - responsible__registration_office=value - ) - return qs - - -class ConservationOfficeTableFilterMixin(django_filters.FilterSet): - """ A mixin for AbstractTableFilter - - Specialized on filtering for related conservation offices - - """ - co = django_filters.ModelChoiceFilter( - method="filter_cons_office", - label=_(""), - label_suffix=_(""), - queryset=KonovaCode.objects.filter( - is_archived=False, - is_leaf=True, - code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID], - ), - widget=ModelSelect2( - url="codes-conservation-office-autocomplete", - attrs={ - "data-placeholder": _("Conservation office"), - "title": _("Search for conservation office"), - "class": "", - } - ), - ) - - def filter_cons_office(self, queryset, name, value): - qs = queryset.filter( - responsible__conservation_office=value - ) - return qs diff --git a/konova/filters/mixins/__init__.py b/konova/filters/mixins/__init__.py new file mode 100644 index 00000000..ca978536 --- /dev/null +++ b/konova/filters/mixins/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" diff --git a/konova/filters/mixins/file_number.py b/konova/filters/mixins/file_number.py new file mode 100644 index 00000000..fa14b93b --- /dev/null +++ b/konova/filters/mixins/file_number.py @@ -0,0 +1,33 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.db.models import QuerySet, Q +from django.utils.translation import gettext_lazy as _ +import django_filters + + +class FileNumberTableFilterMixin(django_filters.FilterSet): + rf = django_filters.CharFilter( + method="filter_file_number", + label=_(""), + label_suffix=_(""), + widget=forms.TextInput( + attrs={ + "placeholder": _("File number"), + "title": _("Search for file number"), + "class": "form-control", + } + ), + ) + + def filter_file_number(self, queryset, name, value) -> QuerySet: + queryset = queryset.filter( + Q(responsible__registration_file_number__icontains=value) | + Q(responsible__conservation_file_number__icontains=value) + ) + return queryset diff --git a/konova/filters/mixins/geo_reference.py b/konova/filters/mixins/geo_reference.py new file mode 100644 index 00000000..eacf5c5b --- /dev/null +++ b/konova/filters/mixins/geo_reference.py @@ -0,0 +1,209 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.db.models import QuerySet, Q +from django.utils.translation import gettext_lazy as _ +import django_filters + +from konova.models import Parcel, District + + +class GeoReferencedTableFilterMixin(django_filters.FilterSet): + """ A mixin for AbstractTableFilter + + Specialized on filtering GeoReferenced model types + + """ + # Parcel gmrkng + di = django_filters.CharFilter( + method="filter_district", + label=_(""), + label_suffix=_(""), + widget=forms.TextInput( + attrs={ + "placeholder": _("District"), + "title": _("Search for district"), + "class": "form-control", + } + ), + ) + # Parcel gmrkng + pg = django_filters.CharFilter( + method="filter_gmrkng", + label=_(""), + label_suffix=_(""), + widget=forms.TextInput( + attrs={ + "placeholder": _("Parcel gmrkng"), + "title": _("Search for parcel gmrkng"), + "class": "form-control", + } + ), + ) + # Parcel + p = django_filters.CharFilter( + method="filter_parcel", + label=_(""), + label_suffix=_(""), + widget=forms.TextInput( + attrs={ + "placeholder": _("Parcel"), + "title": _("Search for parcel"), + "class": "form-control", + } + ), + ) + # Parcel counter + pc = django_filters.CharFilter( + method="filter_parcel_counter", + label=_(""), + label_suffix=_(""), + widget=forms.TextInput( + attrs={ + "placeholder": _("Parcel counter"), + "title": _("Search for parcel counter"), + "class": "form-control", + } + ), + ) + + # Parcel counter + pn = django_filters.CharFilter( + method="filter_parcel_number", + label=_(""), + label_suffix=_(""), + widget=forms.TextInput( + attrs={ + "placeholder": _("Parcel number"), + "title": _("Search for parcel number"), + "class": "form-control", + } + ), + ) + + class Meta: + abstract = True + + def _filter_parcel_reference(self, queryset, filter_q) -> QuerySet: + """ Filters the parcel entries by a given filter_q + + Args: + queryset (QuerySet): The queryset + filter_q (Q): The Q-style filter expression + + Returns: + + """ + matching_parcels = Parcel.objects.filter( + filter_q + ) + + related_geoms = matching_parcels.values( + "geometries" + ).distinct() + queryset = queryset.filter( + geometry__id__in=related_geoms + ) + return queryset + + def filter_district(self, queryset, name, value) -> QuerySet: + """ Filters queryset depending on value for 'Gemarkung' + + Args: + queryset (): + name (): + value (): + + Returns: + + """ + matching_districts = District.objects.filter( + Q(name__icontains=value) | + Q(key__icontains=value) + ).distinct() + matching_parcels = Parcel.objects.filter( + district__in=matching_districts + ) + related_geoms = matching_parcels.values( + "geometries" + ).distinct() + queryset = queryset.filter( + geometry__id__in=related_geoms + ) + return queryset + + def filter_gmrkng(self, queryset, name, value) -> QuerySet: + """ Filters queryset depending on value for 'Gemarkung' + + Args: + queryset (): + name (): + value (): + + Returns: + + """ + queryset = self._filter_parcel_reference( + queryset, + Q(parcel_group__name__icontains=value) | Q(parcel_group__key__icontains=value), + ) + return queryset + + def filter_parcel(self, queryset, name, value) -> QuerySet: + """ Filters queryset depending on value for 'Parcel' + + Args: + queryset (): + name (): + value (): + + Returns: + + """ + value = value.replace("-", "") + queryset = self._filter_parcel_reference( + queryset, + Q(flr=value), + ) + return queryset + + def filter_parcel_counter(self, queryset, name, value) -> QuerySet: + """ Filters queryset depending on value for 'Parcel' + + Args: + queryset (): + name (): + value (): + + Returns: + + """ + value = value.replace("-", "") + queryset = self._filter_parcel_reference( + queryset, + Q(flrstck_zhlr=value) + ) + return queryset + + def filter_parcel_number(self, queryset, name, value) -> QuerySet: + """ Filters queryset depending on value for 'Parcel' + + Args: + queryset (): + name (): + value (): + + Returns: + + """ + value = value.replace("-", "") + queryset = self._filter_parcel_reference( + queryset, + Q(flrstck_nnr=value), + ) + return queryset diff --git a/konova/filters/mixins/keyword.py b/konova/filters/mixins/keyword.py new file mode 100644 index 00000000..212599c8 --- /dev/null +++ b/konova/filters/mixins/keyword.py @@ -0,0 +1,41 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +import django_filters +from django.db.models import QuerySet, Q + +from intervention.inputs import DummyFilterInput + + +class KeywordTableFilterMixin(django_filters.FilterSet): + q = django_filters.Filter( + method='filter_by_keyword', + # Since we use a custom search bar in the template, we need to 'render' this filter + # as 'anonymous' HiddenInput (no id, no name). This way our custom search bar's id and name won't be + # overwritten with these id and name (which would be equal) + # This way we can use the simple filter method mapping for a parameter without using a predefined widget! + widget=DummyFilterInput(), + ) + + class Meta: + abstract = True + + def filter_by_keyword(self, queryset, name, value) -> QuerySet: + """ Filters queryset depending on value of search bar input + + Args: + queryset (): + name (): + value (): + + Returns: + + """ + value = value.strip() + # build filter expression + q = Q(title__icontains=value) | Q(identifier__icontains=value) + return queryset.filter(q) diff --git a/konova/filters/mixins/office.py b/konova/filters/mixins/office.py new file mode 100644 index 00000000..856ff6f3 --- /dev/null +++ b/konova/filters/mixins/office.py @@ -0,0 +1,77 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from dal_select2.widgets import ModelSelect2 +from django.utils.translation import gettext_lazy as _ +import django_filters + +from codelist.models import KonovaCode +from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID + + +class ConservationOfficeTableFilterMixin(django_filters.FilterSet): + """ A mixin for AbstractTableFilter + + Specialized on filtering for related conservation offices + + """ + co = django_filters.ModelChoiceFilter( + method="filter_cons_office", + label=_(""), + label_suffix=_(""), + queryset=KonovaCode.objects.filter( + is_archived=False, + is_leaf=True, + code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID], + ), + widget=ModelSelect2( + url="codes-conservation-office-autocomplete", + attrs={ + "data-placeholder": _("Conservation office"), + "title": _("Search for conservation office"), + "class": "", + } + ), + ) + + def filter_cons_office(self, queryset, name, value): + qs = queryset.filter( + responsible__conservation_office=value + ) + return qs + + +class RegistrationOfficeTableFilterMixin(django_filters.FilterSet): + """ A mixin for AbstractTableFilter + + Specialized on filtering for related registration offices + + """ + ro = django_filters.ModelChoiceFilter( + method="filter_reg_office", + label=_(""), + label_suffix=_(""), + queryset=KonovaCode.objects.filter( + is_archived=False, + is_leaf=True, + code_lists__in=[CODELIST_REGISTRATION_OFFICE_ID], + ), + widget=ModelSelect2( + url="codes-registration-office-autocomplete", + attrs={ + "data-placeholder": _("Registration office"), + "title": _("Search for registration office"), + "class": "", + } + ), + ) + + def filter_reg_office(self, queryset, name, value): + qs = queryset.filter( + responsible__registration_office=value + ) + return qs diff --git a/konova/filters/mixins/record.py b/konova/filters/mixins/record.py new file mode 100644 index 00000000..45476078 --- /dev/null +++ b/konova/filters/mixins/record.py @@ -0,0 +1,51 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ +import django_filters + + +class RecordableTableFilterMixin(django_filters.FilterSet): + """ A mixin for AbstractTableFilter + + Specialized on filtering recordable model types + + """ + sr = django_filters.BooleanFilter( + method='filter_show_recorded', + label=_("Show recorded"), + label_suffix=_(""), + widget=forms.CheckboxInput( + attrs={ + "class": "form-check-input", + } + ) + ) + + class Meta: + abstract = True + + def filter_show_recorded(self, queryset, name, value) -> QuerySet: + """ Filters queryset depending on value of 'show_recorded' setting + + Args: + queryset (): + name (): + value (): + + Returns: + + """ + if not value: + return queryset.filter( + recorded=None, + ) + else: + return queryset + diff --git a/konova/filters/mixins/share.py b/konova/filters/mixins/share.py new file mode 100644 index 00000000..562263c6 --- /dev/null +++ b/konova/filters/mixins/share.py @@ -0,0 +1,57 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.db.models import Q, QuerySet +from django.utils.translation import gettext_lazy as _ +import django_filters + + +class ShareableTableFilterMixin(django_filters.FilterSet): + """ A mixin for AbstractTableFilter + + Specialized on filtering shareable model types + + """ + sa = django_filters.BooleanFilter( + method='filter_show_all', + label=_("Show unshared"), + label_suffix=_(""), + widget=forms.CheckboxInput( + attrs={ + "class": "form-check-input", + } + ) + ) + + class Meta: + abstract = True + + def __init__(self, *args, **kwargs): + self.user = kwargs.pop("user", None) + if self.user is None: + raise AttributeError("User must be set for further filtering!") + super().__init__(*args, **kwargs) + + def filter_show_all(self, queryset, name, value) -> QuerySet: + """ Filters queryset depending on value of 'show_all' setting + + Args: + queryset (): + name (): + value (): + + Returns: + + """ + if not value: + return queryset.filter( + Q(users__in=[self.user]) | # requesting user has access + Q(teams__in=self.user.shared_teams) + ).distinct() + else: + return queryset \ No newline at end of file diff --git a/konova/filters/table_filters.py b/konova/filters/table_filters.py index 8dca6d5f..5b8a4cc1 100644 --- a/konova/filters/table_filters.py +++ b/konova/filters/table_filters.py @@ -7,9 +7,12 @@ Created on: 12.01.22 """ import django_filters -from konova.filters.mixins import RegistrationOfficeTableFilterMixin, ConservationOfficeTableFilterMixin, \ - KeywordTableFilterMixin, FileNumberTableFilterMixin, GeoReferencedTableFilterMixin, ShareableTableFilterMixin, \ - RecordableTableFilterMixin +from konova.filters.mixins.file_number import FileNumberTableFilterMixin +from konova.filters.mixins.geo_reference import GeoReferencedTableFilterMixin +from konova.filters.mixins.keyword import KeywordTableFilterMixin +from konova.filters.mixins.office import ConservationOfficeTableFilterMixin, RegistrationOfficeTableFilterMixin +from konova.filters.mixins.record import RecordableTableFilterMixin +from konova.filters.mixins.share import ShareableTableFilterMixin class AbstractTableFilter(django_filters.FilterSet): From 87b1da8fdd867e7c49828c014fb7110e571ee1de Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 18 Aug 2022 11:25:06 +0200 Subject: [PATCH 16/48] Autocomplete refactoring * refactors konova/autocompletes.py by splitting into individual files and moving them to fitting apps * autocomplete files now live in APPNAME/autocomplete/... --- analysis/forms.py | 2 +- codelist/autocomplete/__init__.py | 7 + codelist/autocomplete/base.py | 74 ++++ codelist/autocomplete/biotope.py | 110 +++++ codelist/autocomplete/compensation_action.py | 45 ++ codelist/autocomplete/handler.py | 24 ++ codelist/autocomplete/law.py | 24 ++ codelist/autocomplete/office.py | 41 ++ codelist/autocomplete/process_type.py | 21 + codelist/urls.py | 18 +- compensation/autocomplete/__init__.py | 7 + compensation/autocomplete/eco_account.py | 34 ++ compensation/forms/compensation.py | 2 +- compensation/forms/mixins.py | 4 +- .../forms/modals/compensation_action.py | 2 +- compensation/forms/modals/state.py | 2 +- compensation/urls/eco_account.py | 4 + intervention/autocomplete/__init__.py | 7 + intervention/autocomplete/intervention.py | 36 ++ intervention/forms/intervention.py | 10 +- intervention/forms/modals/deduction.py | 4 +- intervention/forms/modals/share.py | 4 +- intervention/urls.py | 4 + konova/autocompletes.py | 401 ------------------ konova/filters/mixins/office.py | 4 +- konova/tests/test_autocompletes.py | 28 +- konova/tests/test_views.py | 18 +- konova/urls.py | 21 - user/autocomplete/__init__.py | 7 + user/autocomplete/share.py | 52 +++ user/autocomplete/team.py | 34 ++ user/forms/modals/team.py | 4 +- user/urls.py | 6 + 33 files changed, 596 insertions(+), 465 deletions(-) create mode 100644 codelist/autocomplete/__init__.py create mode 100644 codelist/autocomplete/base.py create mode 100644 codelist/autocomplete/biotope.py create mode 100644 codelist/autocomplete/compensation_action.py create mode 100644 codelist/autocomplete/handler.py create mode 100644 codelist/autocomplete/law.py create mode 100644 codelist/autocomplete/office.py create mode 100644 codelist/autocomplete/process_type.py create mode 100644 compensation/autocomplete/__init__.py create mode 100644 compensation/autocomplete/eco_account.py create mode 100644 intervention/autocomplete/__init__.py create mode 100644 intervention/autocomplete/intervention.py delete mode 100644 konova/autocompletes.py create mode 100644 user/autocomplete/__init__.py create mode 100644 user/autocomplete/share.py create mode 100644 user/autocomplete/team.py diff --git a/analysis/forms.py b/analysis/forms.py index 68e217af..c9a7995a 100644 --- a/analysis/forms.py +++ b/analysis/forms.py @@ -55,7 +55,7 @@ class TimespanReportForm(BaseForm): code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID], ), widget=autocomplete.ModelSelect2( - url="codes-conservation-office-autocomplete", + url="codelist:conservation-office-autocomplete", attrs={ "data-placeholder": _("Click for selection") } diff --git a/codelist/autocomplete/__init__.py b/codelist/autocomplete/__init__.py new file mode 100644 index 00000000..ca978536 --- /dev/null +++ b/codelist/autocomplete/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" diff --git a/codelist/autocomplete/base.py b/codelist/autocomplete/base.py new file mode 100644 index 00000000..7e7a8e4a --- /dev/null +++ b/codelist/autocomplete/base.py @@ -0,0 +1,74 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from dal_select2.views import Select2GroupQuerySetView +from django.db.models import Q + +from codelist.models import KonovaCode + + +class KonovaCodeAutocomplete(Select2GroupQuerySetView): + """ + Provides simple autocomplete functionality for codes + + Parameter support: + * q: Search for a word inside long_name of a code + * c: Search inside a special codelist + + """ + paginate_by = 50 + + def order_by(self, qs): + """ Orders by a predefined value + + Wrapped in a function to provide inheritance-based different orders + + Args: + qs (QuerySet): The queryset to be ordered + + Returns: + qs (QuerySet): The ordered queryset + """ + return qs.order_by( + "long_name" + ) + + def get_queryset(self): + if self.request.user.is_anonymous: + return KonovaCode.objects.none() + qs = KonovaCode.objects.filter( + is_archived=False, + is_selectable=True, + is_leaf=True, + ) + qs = self.order_by(qs) + if self.c: + qs = qs.filter( + code_lists__in=[self.c] + ) + if self.q: + # Remove whitespaces from self.q and split input in all keywords (if multiple given) + q = dict.fromkeys(self.q.strip().split(" ")) + # Create one filter looking up for all keys where all keywords can be found in the same result + _filter = Q() + for keyword in q: + q_or = Q() + q_or |= Q(long_name__icontains=keyword) + q_or |= Q(short_name__icontains=keyword) + q_or |= Q(parent__long_name__icontains=keyword) + q_or |= Q(parent__short_name__icontains=keyword) + q_or |= Q(parent__parent__long_name__icontains=keyword) + q_or |= Q(parent__parent__short_name__icontains=keyword) + _filter.add(q_or, Q.AND) + qs = qs.filter(_filter).distinct() + return qs + + def get_result_label(self, result): + return f"{result.long_name}" + + def get_selected_result_label(self, result): + return f"{result.__str__()}" diff --git a/codelist/autocomplete/biotope.py b/codelist/autocomplete/biotope.py new file mode 100644 index 00000000..5e56b7f3 --- /dev/null +++ b/codelist/autocomplete/biotope.py @@ -0,0 +1,110 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +import collections + +from django.core.exceptions import ImproperlyConfigured + +from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID +from codelist.autocomplete.base import KonovaCodeAutocomplete +from konova.utils.message_templates import UNGROUPED + + +class BiotopeCodeAutocomplete(KonovaCodeAutocomplete): + """ + Due to limitations of the django dal package, we need to subclass for each code list + """ + group_by_related = "parent" + related_field_name = "long_name" + + def __init__(self, *args, **kwargs): + self.c = CODELIST_BIOTOPES_ID + super().__init__(*args, **kwargs) + + def order_by(self, qs): + """ Orders by a predefined value + + Wrapped in a function to provide inheritance-based different orders + + Args: + qs (QuerySet): The queryset to be ordered + + Returns: + qs (QuerySet): The ordered queryset + """ + return qs.order_by( + "short_name", + ) + + def get_result_label(self, result): + return f"{result.long_name} ({result.short_name})" + + def get_results(self, context): + """Return the options grouped by a common related model. + + Raises ImproperlyConfigured if self.group_by_name is not configured + """ + if not self.group_by_related: + raise ImproperlyConfigured("Missing group_by_related.") + + super_groups = collections.OrderedDict() + + object_list = context['object_list'] + + for result in object_list: + group = result.parent if result.parent else None + group_name = f"{group.long_name} ({group.short_name})" if group else UNGROUPED + super_group = result.parent.parent if result.parent else None + super_group_name = f"{super_group.long_name} ({super_group.short_name})" if super_group else UNGROUPED + super_groups.setdefault(super_group_name, {}) + super_groups[super_group_name].setdefault(group_name, []) + super_groups[super_group_name][group_name].append(result) + + return [{ + 'id': None, + 'text': super_group, + 'children': [{ + "id": None, + "text": group, + "children": [{ + 'id': self.get_result_value(result), + 'text': self.get_result_label(result), + 'selected_text': self.get_selected_result_label(result), + } for result in results] + } for group, results in groups.items()] + } for super_group, groups in super_groups.items()] + + +class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete): + """ + Due to limitations of the django dal package, we need to subclass for each code list + """ + group_by_related = "parent" + related_field_name = "long_name" + paginate_by = 200 + + def __init__(self, *args, **kwargs): + self.c = CODELIST_BIOTOPES_EXTRA_CODES_ID + super().__init__(*args, **kwargs) + + def order_by(self, qs): + """ Orders by a predefined value + + Wrapped in a function to provide inheritance-based different orders + + Args: + qs (QuerySet): The queryset to be ordered + + Returns: + qs (QuerySet): The ordered queryset + """ + return qs.order_by( + "long_name", + ) + + def get_result_label(self, result): + return f"{result.long_name} ({result.short_name})" diff --git a/codelist/autocomplete/compensation_action.py b/codelist/autocomplete/compensation_action.py new file mode 100644 index 00000000..47a652cd --- /dev/null +++ b/codelist/autocomplete/compensation_action.py @@ -0,0 +1,45 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID +from codelist.autocomplete.base import KonovaCodeAutocomplete + + +class CompensationActionCodeAutocomplete(KonovaCodeAutocomplete): + """ + Due to limitations of the django dal package, we need to subclass for each code list + """ + group_by_related = "parent" + related_field_name = "long_name" + + def __init__(self, *args, **kwargs): + self.c = CODELIST_COMPENSATION_ACTION_ID + super().__init__(*args, **kwargs) + + def order_by(self, qs): + return qs.order_by( + "parent__long_name" + ) + + +class CompensationActionDetailCodeAutocomplete(KonovaCodeAutocomplete): + """ + Due to limitations of the django dal package, we need to subclass for each code list + """ + group_by_related = "parent" + related_field_name = "long_name" + paginate_by = 200 + + def __init__(self, *args, **kwargs): + self.c = CODELIST_COMPENSATION_ACTION_DETAIL_ID + super().__init__(*args, **kwargs) + + def order_by(self, qs): + return qs.order_by( + "long_name" + ) + diff --git a/codelist/autocomplete/handler.py b/codelist/autocomplete/handler.py new file mode 100644 index 00000000..480d3315 --- /dev/null +++ b/codelist/autocomplete/handler.py @@ -0,0 +1,24 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from codelist.settings import CODELIST_HANDLER_ID +from codelist.autocomplete.base import KonovaCodeAutocomplete + + +class HandlerCodeAutocomplete(KonovaCodeAutocomplete): + """ + Due to limitations of the django dal package, we need to subclass for each code list + """ + group_by_related = "parent" + related_field_name = "long_name" + + def __init__(self, *args, **kwargs): + self.c = CODELIST_HANDLER_ID + super().__init__(*args, **kwargs) + + def get_result_label(self, result): + return result.long_name diff --git a/codelist/autocomplete/law.py b/codelist/autocomplete/law.py new file mode 100644 index 00000000..cf3f4b54 --- /dev/null +++ b/codelist/autocomplete/law.py @@ -0,0 +1,24 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from codelist.settings import CODELIST_LAW_ID +from codelist.autocomplete.base import KonovaCodeAutocomplete + + +class LawCodeAutocomplete(KonovaCodeAutocomplete): + """ + Due to limitations of the django dal package, we need to subclass for each code list + """ + group_by_related = "parent" + related_field_name = "long_name" + + def __init__(self, *args, **kwargs): + self.c = CODELIST_LAW_ID + super().__init__(*args, **kwargs) + + def get_result_label(self, result): + return f"{result.long_name} ({result.short_name})" diff --git a/codelist/autocomplete/office.py b/codelist/autocomplete/office.py new file mode 100644 index 00000000..46ddbea0 --- /dev/null +++ b/codelist/autocomplete/office.py @@ -0,0 +1,41 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID +from codelist.autocomplete.base import KonovaCodeAutocomplete + + +class RegistrationOfficeCodeAutocomplete(KonovaCodeAutocomplete): + """ + Due to limitations of the django dal package, we need to subclass for each code list + """ + group_by_related = "parent" + related_field_name = "long_name" + + def __init__(self, *args, **kwargs): + self.c = CODELIST_REGISTRATION_OFFICE_ID + super().__init__(*args, **kwargs) + + def order_by(self, qs): + return qs.order_by( + "parent__long_name" + ) + + +class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete): + """ + Due to limitations of the django dal package, we need to subclass for each code list + """ + group_by_related = "parent" + related_field_name = "long_name" + + def __init__(self, *args, **kwargs): + self.c = CODELIST_CONSERVATION_OFFICE_ID + super().__init__(*args, **kwargs) + + def get_result_label(self, result): + return f"{result.long_name} ({result.short_name})" diff --git a/codelist/autocomplete/process_type.py b/codelist/autocomplete/process_type.py new file mode 100644 index 00000000..3f487e41 --- /dev/null +++ b/codelist/autocomplete/process_type.py @@ -0,0 +1,21 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from codelist.autocomplete.base import KonovaCodeAutocomplete +from codelist.settings import CODELIST_PROCESS_TYPE_ID + + +class ProcessTypeCodeAutocomplete(KonovaCodeAutocomplete): + """ + Due to limitations of the django dal package, we need to subclass for each code list + """ + group_by_related = "parent" + related_field_name = "long_name" + + def __init__(self, *args, **kwargs): + self.c = CODELIST_PROCESS_TYPE_ID + super().__init__(*args, **kwargs) diff --git a/codelist/urls.py b/codelist/urls.py index 7d2dbba2..bd230f91 100644 --- a/codelist/urls.py +++ b/codelist/urls.py @@ -7,8 +7,24 @@ Created on: 23.08.21 """ from django.urls import path +from codelist.autocomplete.biotope import BiotopeCodeAutocomplete, BiotopeExtraCodeAutocomplete +from codelist.autocomplete.compensation_action import CompensationActionDetailCodeAutocomplete, \ + CompensationActionCodeAutocomplete +from codelist.autocomplete.handler import HandlerCodeAutocomplete +from codelist.autocomplete.law import LawCodeAutocomplete +from codelist.autocomplete.office import ConservationOfficeCodeAutocomplete, RegistrationOfficeCodeAutocomplete +from codelist.autocomplete.process_type import ProcessTypeCodeAutocomplete app_name = "codelist" urlpatterns = [ - + path("atcmplt/codes/biotope", BiotopeCodeAutocomplete.as_view(), name="biotope-autocomplete"), + path("atcmplt/codes/biotope/extra", BiotopeExtraCodeAutocomplete.as_view(), + name="biotope-extra-type-autocomplete"), + path("atcmplt/codes/law", LawCodeAutocomplete.as_view(), name="law-autocomplete"), + path("atcmplt/codes/reg-off", RegistrationOfficeCodeAutocomplete.as_view(), name="registration-office-autocomplete"), + path("atcmplt/codes/cons-off", ConservationOfficeCodeAutocomplete.as_view(), name="conservation-office-autocomplete"), + path("atcmplt/codes/handler", HandlerCodeAutocomplete.as_view(), name="handler-autocomplete"), + path("atcmplt/codes/comp/action", CompensationActionCodeAutocomplete.as_view(), name="compensation-action-autocomplete"), + path("atcmplt/codes/comp/action/detail", CompensationActionDetailCodeAutocomplete.as_view(), name="compensation-action-detail-autocomplete"), + path("atcmplt/codes/prc-type", ProcessTypeCodeAutocomplete.as_view(), name="process-type-autocomplete"), ] \ No newline at end of file diff --git a/compensation/autocomplete/__init__.py b/compensation/autocomplete/__init__.py new file mode 100644 index 00000000..ca978536 --- /dev/null +++ b/compensation/autocomplete/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" diff --git a/compensation/autocomplete/eco_account.py b/compensation/autocomplete/eco_account.py new file mode 100644 index 00000000..d1ea90f4 --- /dev/null +++ b/compensation/autocomplete/eco_account.py @@ -0,0 +1,34 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from dal_select2.views import Select2QuerySetView +from django.db.models import Q + +from compensation.models import EcoAccount + + +class EcoAccountAutocomplete(Select2QuerySetView): + """ Autocomplete for ecoAccount entries + + Only returns entries that are already recorded and not deleted + + """ + def get_queryset(self): + if self.request.user.is_anonymous: + return EcoAccount.objects.none() + qs = EcoAccount.objects.filter( + deleted=None, + recorded__isnull=False, + ).order_by( + "identifier" + ) + if self.q: + qs = qs.filter( + Q(identifier__icontains=self.q) | + Q(title__icontains=self.q) + ).distinct() + return qs diff --git a/compensation/forms/compensation.py b/compensation/forms/compensation.py index 1672e161..c5186793 100644 --- a/compensation/forms/compensation.py +++ b/compensation/forms/compensation.py @@ -88,7 +88,7 @@ class NewCompensationForm(AbstractCompensationForm, deleted=None, ), widget=autocomplete.ModelSelect2( - url="interventions-autocomplete", + url="intervention:autocomplete", attrs={ "data-placeholder": _("Click for selection"), "data-minimum-input-length": 3, diff --git a/compensation/forms/mixins.py b/compensation/forms/mixins.py index db174808..7cb7eb25 100644 --- a/compensation/forms/mixins.py +++ b/compensation/forms/mixins.py @@ -27,7 +27,7 @@ class CompensationResponsibleFormMixin(forms.Form): code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID], ), widget=autocomplete.ModelSelect2( - url="codes-conservation-office-autocomplete", + url="codelist:conservation-office-autocomplete", attrs={ "data-placeholder": _("Click for selection") } @@ -57,7 +57,7 @@ class CompensationResponsibleFormMixin(forms.Form): code_lists__in=[CODELIST_HANDLER_ID], ), widget=autocomplete.ModelSelect2( - url="codes-handler-autocomplete", + url="codelist:handler-autocomplete", attrs={ "data-placeholder": _("Click for selection"), } diff --git a/compensation/forms/modals/compensation_action.py b/compensation/forms/modals/compensation_action.py index cc0bae15..6cd7279f 100644 --- a/compensation/forms/modals/compensation_action.py +++ b/compensation/forms/modals/compensation_action.py @@ -44,7 +44,7 @@ class NewCompensationActionModalForm(BaseModalForm): code_lists__in=[CODELIST_COMPENSATION_ACTION_DETAIL_ID], ), widget=autocomplete.ModelSelect2Multiple( - url="codes-compensation-action-detail-autocomplete", + url="codelist:compensation-action-detail-autocomplete", attrs={ "data-placeholder": _("Action Type detail"), } diff --git a/compensation/forms/modals/state.py b/compensation/forms/modals/state.py index 485045ba..f0c42213 100644 --- a/compensation/forms/modals/state.py +++ b/compensation/forms/modals/state.py @@ -46,7 +46,7 @@ class NewCompensationStateModalForm(BaseModalForm): code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID], ), widget=autocomplete.ModelSelect2Multiple( - url="codes-biotope-extra-type-autocomplete", + url="codelist:biotope-extra-type-autocomplete", attrs={ "data-placeholder": _("Biotope additional type"), } diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index 5a84e8ca..c57540e5 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -6,6 +6,8 @@ Created on: 24.08.21 """ from django.urls import path + +from compensation.autocomplete.eco_account import EcoAccountAutocomplete from compensation.views.eco_account import * app_name = "acc" @@ -47,4 +49,6 @@ urlpatterns = [ path('/deduction//edit', deduction_edit_view, name='edit-deduction'), path('/deduct/new', new_deduction_view, name='new-deduction'), + # Autocomplete + path("atcmplt/eco-accounts", EcoAccountAutocomplete.as_view(), name="autocomplete"), ] \ No newline at end of file diff --git a/intervention/autocomplete/__init__.py b/intervention/autocomplete/__init__.py new file mode 100644 index 00000000..ca978536 --- /dev/null +++ b/intervention/autocomplete/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" diff --git a/intervention/autocomplete/intervention.py b/intervention/autocomplete/intervention.py new file mode 100644 index 00000000..961c9660 --- /dev/null +++ b/intervention/autocomplete/intervention.py @@ -0,0 +1,36 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from dal_select2.views import Select2QuerySetView +from django.db.models import Q + +from intervention.models import Intervention + + +class InterventionAutocomplete(Select2QuerySetView): + """ Autocomplete for intervention entries + + Only returns entries that are accessible for the requesting user + + """ + def get_queryset(self): + user = self.request.user + if user.is_anonymous: + return Intervention.objects.none() + qs = Intervention.objects.filter( + Q(deleted=None) & + Q(users__in=[user]) | + Q(teams__in=user.teams.all()) + ).order_by( + "identifier" + ).distinct() + if self.q: + qs = qs.filter( + Q(identifier__icontains=self.q) | + Q(title__icontains=self.q) + ).distinct() + return qs diff --git a/intervention/forms/intervention.py b/intervention/forms/intervention.py index 15b02fd7..c62b3faa 100644 --- a/intervention/forms/intervention.py +++ b/intervention/forms/intervention.py @@ -60,7 +60,7 @@ class NewInterventionForm(BaseForm): code_lists__in=[CODELIST_PROCESS_TYPE_ID], ), widget=autocomplete.ModelSelect2( - url="codes-process-type-autocomplete", + url="codelist:process-type-autocomplete", attrs={ "data-placeholder": _("Click for selection"), } @@ -77,7 +77,7 @@ class NewInterventionForm(BaseForm): code_lists__in=[CODELIST_LAW_ID], ), widget=autocomplete.ModelSelect2Multiple( - url="codes-law-autocomplete", + url="codelist:law-autocomplete", attrs={ "data-placeholder": _("Click for selection"), } @@ -93,7 +93,7 @@ class NewInterventionForm(BaseForm): code_lists__in=[CODELIST_REGISTRATION_OFFICE_ID], ), widget=autocomplete.ModelSelect2( - url="codes-registration-office-autocomplete", + url="codelist:registration-office-autocomplete", attrs={ "data-placeholder": _("Click for selection"), } @@ -109,7 +109,7 @@ class NewInterventionForm(BaseForm): code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID], ), widget=autocomplete.ModelSelect2( - url="codes-conservation-office-autocomplete", + url="codelist:conservation-office-autocomplete", attrs={ "data-placeholder": _("Click for selection"), } @@ -150,7 +150,7 @@ class NewInterventionForm(BaseForm): code_lists__in=[CODELIST_HANDLER_ID], ), widget=autocomplete.ModelSelect2( - url="codes-handler-autocomplete", + url="codelist:handler-autocomplete", attrs={ "data-placeholder": _("Click for selection"), } diff --git a/intervention/forms/modals/deduction.py b/intervention/forms/modals/deduction.py index 2e182146..130ef2f8 100644 --- a/intervention/forms/modals/deduction.py +++ b/intervention/forms/modals/deduction.py @@ -33,7 +33,7 @@ class NewEcoAccountDeductionModalForm(BaseModalForm): help_text=_("Only recorded accounts can be selected for deductions"), queryset=EcoAccount.objects.filter(deleted=None), widget=autocomplete.ModelSelect2( - url="accounts-autocomplete", + url="compensation:acc:autocomplete", attrs={ "data-placeholder": _("Eco-account"), "data-minimum-input-length": 3, @@ -60,7 +60,7 @@ class NewEcoAccountDeductionModalForm(BaseModalForm): help_text=_("Only shared interventions can be selected"), queryset=Intervention.objects.filter(deleted=None), widget=autocomplete.ModelSelect2( - url="interventions-autocomplete", + url="intervention:autocomplete", attrs={ "data-placeholder": _("Intervention"), "data-minimum-input-length": 3, diff --git a/intervention/forms/modals/share.py b/intervention/forms/modals/share.py index 39c9d142..35c662d5 100644 --- a/intervention/forms/modals/share.py +++ b/intervention/forms/modals/share.py @@ -36,7 +36,7 @@ class ShareModalForm(BaseModalForm): required=False, queryset=Team.objects.all(), widget=autocomplete.ModelSelect2Multiple( - url="share-team-autocomplete", + url="user:share-team-autocomplete", attrs={ "data-placeholder": _("Click for selection"), "data-minimum-input-length": 3, @@ -50,7 +50,7 @@ class ShareModalForm(BaseModalForm): required=False, queryset=User.objects.all(), widget=autocomplete.ModelSelect2Multiple( - url="share-user-autocomplete", + url="user:share-user-autocomplete", attrs={ "data-placeholder": _("Click for selection"), "data-minimum-input-length": 3, diff --git a/intervention/urls.py b/intervention/urls.py index c7c43837..b3cc7038 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -7,6 +7,7 @@ Created on: 30.11.20 """ from django.urls import path +from intervention.autocomplete.intervention import InterventionAutocomplete from intervention.views import index_view, new_view, detail_view, edit_view, remove_view, new_document_view, share_view, \ create_share_view, remove_revocation_view, new_revocation_view, check_view, log_view, new_deduction_view, \ record_view, remove_document_view, get_document_view, get_revocation_view, new_id_view, report_view, \ @@ -48,4 +49,7 @@ urlpatterns = [ path('/revocation//edit', edit_revocation_view, name='edit-revocation'), path('/revocation//remove', remove_revocation_view, name='remove-revocation'), path('revocation/', get_revocation_view, name='get-doc-revocation'), + + # Autocomplete + path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="autocomplete"), ] \ No newline at end of file diff --git a/konova/autocompletes.py b/konova/autocompletes.py deleted file mode 100644 index fbd92f75..00000000 --- a/konova/autocompletes.py +++ /dev/null @@ -1,401 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 07.12.20 - -""" -import collections - -from dal_select2.views import Select2QuerySetView, Select2GroupQuerySetView -from django.core.exceptions import ImproperlyConfigured - -from konova.utils.message_templates import UNGROUPED -from user.models import User, Team -from django.db.models import Q - -from codelist.models import KonovaCode -from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, \ - CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_PROCESS_TYPE_ID, \ - CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_HANDLER_ID -from compensation.models import EcoAccount -from intervention.models import Intervention - - -class EcoAccountAutocomplete(Select2QuerySetView): - """ Autocomplete for ecoAccount entries - - Only returns entries that are already recorded and not deleted - - """ - def get_queryset(self): - if self.request.user.is_anonymous: - return EcoAccount.objects.none() - qs = EcoAccount.objects.filter( - deleted=None, - recorded__isnull=False, - ).order_by( - "identifier" - ) - if self.q: - qs = qs.filter( - Q(identifier__icontains=self.q) | - Q(title__icontains=self.q) - ).distinct() - return qs - - -class InterventionAutocomplete(Select2QuerySetView): - """ Autocomplete for intervention entries - - Only returns entries that are accessible for the requesting user - - """ - def get_queryset(self): - user = self.request.user - if user.is_anonymous: - return Intervention.objects.none() - qs = Intervention.objects.filter( - Q(deleted=None) & - Q(users__in=[user]) | - Q(teams__in=user.teams.all()) - ).order_by( - "identifier" - ).distinct() - if self.q: - qs = qs.filter( - Q(identifier__icontains=self.q) | - Q(title__icontains=self.q) - ).distinct() - return qs - - -class ShareUserAutocomplete(Select2QuerySetView): - """ Autocomplete for share with single users - - - """ - def get_queryset(self): - if self.request.user.is_anonymous: - return User.objects.none() - qs = User.objects.all() - if self.q: - # Due to privacy concerns only a full username match will return the proper user entry - qs = qs.filter( - Q(username=self.q) | - Q(email=self.q) - ).distinct() - qs = qs.order_by("username") - return qs - - -class ShareTeamAutocomplete(Select2QuerySetView): - """ Autocomplete for share with teams - - """ - def get_queryset(self): - if self.request.user.is_anonymous: - return Team.objects.none() - qs = Team.objects.filter( - deleted__isnull=True - ) - if self.q: - # Due to privacy concerns only a full username match will return the proper user entry - qs = qs.filter( - name__icontains=self.q - ) - qs = qs.order_by( - "name" - ) - return qs - - -class TeamAdminAutocomplete(Select2QuerySetView): - """ Autocomplete for share with teams - - """ - def get_queryset(self): - if self.request.user.is_anonymous: - return User.objects.none() - qs = User.objects.filter( - id__in=self.forwarded.get("members", []) - ).exclude( - id__in=self.forwarded.get("admins", []) - ) - if self.q: - # Due to privacy concerns only a full username match will return the proper user entry - qs = qs.filter( - name__icontains=self.q - ) - qs = qs.order_by( - "username" - ) - return qs - - -class KonovaCodeAutocomplete(Select2GroupQuerySetView): - """ - Provides simple autocomplete functionality for codes - - Parameter support: - * q: Search for a word inside long_name of a code - * c: Search inside a special codelist - - """ - paginate_by = 50 - - def order_by(self, qs): - """ Orders by a predefined value - - Wrapped in a function to provide inheritance-based different orders - - Args: - qs (QuerySet): The queryset to be ordered - - Returns: - qs (QuerySet): The ordered queryset - """ - return qs.order_by( - "long_name" - ) - - def get_queryset(self): - if self.request.user.is_anonymous: - return KonovaCode.objects.none() - qs = KonovaCode.objects.filter( - is_archived=False, - is_selectable=True, - is_leaf=True, - ) - qs = self.order_by(qs) - if self.c: - qs = qs.filter( - code_lists__in=[self.c] - ) - if self.q: - # Remove whitespaces from self.q and split input in all keywords (if multiple given) - q = dict.fromkeys(self.q.strip().split(" ")) - # Create one filter looking up for all keys where all keywords can be found in the same result - _filter = Q() - for keyword in q: - q_or = Q() - q_or |= Q(long_name__icontains=keyword) - q_or |= Q(short_name__icontains=keyword) - q_or |= Q(parent__long_name__icontains=keyword) - q_or |= Q(parent__short_name__icontains=keyword) - q_or |= Q(parent__parent__long_name__icontains=keyword) - q_or |= Q(parent__parent__short_name__icontains=keyword) - _filter.add(q_or, Q.AND) - qs = qs.filter(_filter).distinct() - return qs - - def get_result_label(self, result): - return f"{result.long_name}" - - def get_selected_result_label(self, result): - return f"{result.__str__()}" - - -class CompensationActionCodeAutocomplete(KonovaCodeAutocomplete): - """ - Due to limitations of the django dal package, we need to subclass for each code list - """ - group_by_related = "parent" - related_field_name = "long_name" - - def __init__(self, *args, **kwargs): - self.c = CODELIST_COMPENSATION_ACTION_ID - super().__init__(*args, **kwargs) - - def order_by(self, qs): - return qs.order_by( - "parent__long_name" - ) - - -class CompensationActionDetailCodeAutocomplete(KonovaCodeAutocomplete): - """ - Due to limitations of the django dal package, we need to subclass for each code list - """ - group_by_related = "parent" - related_field_name = "long_name" - paginate_by = 200 - - def __init__(self, *args, **kwargs): - self.c = CODELIST_COMPENSATION_ACTION_DETAIL_ID - super().__init__(*args, **kwargs) - - def order_by(self, qs): - return qs.order_by( - "long_name" - ) - - -class BiotopeCodeAutocomplete(KonovaCodeAutocomplete): - """ - Due to limitations of the django dal package, we need to subclass for each code list - """ - group_by_related = "parent" - related_field_name = "long_name" - - def __init__(self, *args, **kwargs): - self.c = CODELIST_BIOTOPES_ID - super().__init__(*args, **kwargs) - - def order_by(self, qs): - """ Orders by a predefined value - - Wrapped in a function to provide inheritance-based different orders - - Args: - qs (QuerySet): The queryset to be ordered - - Returns: - qs (QuerySet): The ordered queryset - """ - return qs.order_by( - "short_name", - ) - - def get_result_label(self, result): - return f"{result.long_name} ({result.short_name})" - - def get_results(self, context): - """Return the options grouped by a common related model. - - Raises ImproperlyConfigured if self.group_by_name is not configured - """ - if not self.group_by_related: - raise ImproperlyConfigured("Missing group_by_related.") - - super_groups = collections.OrderedDict() - - object_list = context['object_list'] - - for result in object_list: - group = result.parent if result.parent else None - group_name = f"{group.long_name} ({group.short_name})" if group else UNGROUPED - super_group = result.parent.parent if result.parent else None - super_group_name = f"{super_group.long_name} ({super_group.short_name})" if super_group else UNGROUPED - super_groups.setdefault(super_group_name, {}) - super_groups[super_group_name].setdefault(group_name, []) - super_groups[super_group_name][group_name].append(result) - - return [{ - 'id': None, - 'text': super_group, - 'children': [{ - "id": None, - "text": group, - "children": [{ - 'id': self.get_result_value(result), - 'text': self.get_result_label(result), - 'selected_text': self.get_selected_result_label(result), - } for result in results] - } for group, results in groups.items()] - } for super_group, groups in super_groups.items()] - - -class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete): - """ - Due to limitations of the django dal package, we need to subclass for each code list - """ - group_by_related = "parent" - related_field_name = "long_name" - paginate_by = 200 - - def __init__(self, *args, **kwargs): - self.c = CODELIST_BIOTOPES_EXTRA_CODES_ID - super().__init__(*args, **kwargs) - - def order_by(self, qs): - """ Orders by a predefined value - - Wrapped in a function to provide inheritance-based different orders - - Args: - qs (QuerySet): The queryset to be ordered - - Returns: - qs (QuerySet): The ordered queryset - """ - return qs.order_by( - "long_name", - ) - - def get_result_label(self, result): - return f"{result.long_name} ({result.short_name})" - - -class LawCodeAutocomplete(KonovaCodeAutocomplete): - """ - Due to limitations of the django dal package, we need to subclass for each code list - """ - group_by_related = "parent" - related_field_name = "long_name" - - def __init__(self, *args, **kwargs): - self.c = CODELIST_LAW_ID - super().__init__(*args, **kwargs) - - def get_result_label(self, result): - return f"{result.long_name} ({result.short_name})" - - -class ProcessTypeCodeAutocomplete(KonovaCodeAutocomplete): - """ - Due to limitations of the django dal package, we need to subclass for each code list - """ - group_by_related = "parent" - related_field_name = "long_name" - - def __init__(self, *args, **kwargs): - self.c = CODELIST_PROCESS_TYPE_ID - super().__init__(*args, **kwargs) - - -class RegistrationOfficeCodeAutocomplete(KonovaCodeAutocomplete): - """ - Due to limitations of the django dal package, we need to subclass for each code list - """ - group_by_related = "parent" - related_field_name = "long_name" - - def __init__(self, *args, **kwargs): - self.c = CODELIST_REGISTRATION_OFFICE_ID - super().__init__(*args, **kwargs) - - def order_by(self, qs): - return qs.order_by( - "parent__long_name" - ) - - -class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete): - """ - Due to limitations of the django dal package, we need to subclass for each code list - """ - group_by_related = "parent" - related_field_name = "long_name" - - def __init__(self, *args, **kwargs): - self.c = CODELIST_CONSERVATION_OFFICE_ID - super().__init__(*args, **kwargs) - - def get_result_label(self, result): - return f"{result.long_name} ({result.short_name})" - - -class HandlerCodeAutocomplete(KonovaCodeAutocomplete): - """ - Due to limitations of the django dal package, we need to subclass for each code list - """ - group_by_related = "parent" - related_field_name = "long_name" - - def __init__(self, *args, **kwargs): - self.c = CODELIST_HANDLER_ID - super().__init__(*args, **kwargs) - - def get_result_label(self, result): - return result.long_name diff --git a/konova/filters/mixins/office.py b/konova/filters/mixins/office.py index 856ff6f3..3ca540f9 100644 --- a/konova/filters/mixins/office.py +++ b/konova/filters/mixins/office.py @@ -29,7 +29,7 @@ class ConservationOfficeTableFilterMixin(django_filters.FilterSet): code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID], ), widget=ModelSelect2( - url="codes-conservation-office-autocomplete", + url="codelist:conservation-office-autocomplete", attrs={ "data-placeholder": _("Conservation office"), "title": _("Search for conservation office"), @@ -61,7 +61,7 @@ class RegistrationOfficeTableFilterMixin(django_filters.FilterSet): code_lists__in=[CODELIST_REGISTRATION_OFFICE_ID], ), widget=ModelSelect2( - url="codes-registration-office-autocomplete", + url="codelist:registration-office-autocomplete", attrs={ "data-placeholder": _("Registration office"), "title": _("Search for registration office"), diff --git a/konova/tests/test_autocompletes.py b/konova/tests/test_autocompletes.py index 1533d572..c763fb62 100644 --- a/konova/tests/test_autocompletes.py +++ b/konova/tests/test_autocompletes.py @@ -21,7 +21,7 @@ class AutocompleteTestCase(BaseTestCase): def test_user_autocomplete(self): self.client.login(username=self.superuser.username, password=self.superuser_pw) - user_autocomplete_url = reverse("share-user-autocomplete") + user_autocomplete_url = reverse("user:share-user-autocomplete") username = self.user.username # Provide the full name --> success @@ -60,19 +60,19 @@ class AutocompleteTestCase(BaseTestCase): def test_all_autocompletes(self): tests = [ - "accounts-autocomplete", - "interventions-autocomplete", - "codes-compensation-action-autocomplete", - "codes-compensation-action-detail-autocomplete", - "codes-biotope-autocomplete", - "codes-biotope-extra-type-autocomplete", - "codes-law-autocomplete", - "codes-process-type-autocomplete", - "codes-registration-office-autocomplete", - "codes-conservation-office-autocomplete", - "share-user-autocomplete", - "share-team-autocomplete", - "team-admin-autocomplete", + "compensation:acc:autocomplete", + "intervention:autocomplete", + "codelist:compensation-action-autocomplete", + "codelist:compensation-action-detail-autocomplete", + "codelist:biotope-autocomplete", + "codelist:biotope-extra-type-autocomplete", + "codelist:law-autocomplete", + "codelist:process-type-autocomplete", + "codelist:registration-office-autocomplete", + "codelist:conservation-office-autocomplete", + "user:share-user-autocomplete", + "user:share-team-autocomplete", + "user:team-admin-autocomplete", ] for test in tests: self.client.login(username=self.superuser.username, password=self.superuser_pw) diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index c6006190..32ab5986 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -590,15 +590,15 @@ class AutocompleteTestCase(BaseViewTestCase): @classmethod def setUpTestData(cls) -> None: super().setUpTestData() - cls.atcmplt_accs = reverse("accounts-autocomplete") - cls.atcmplt_interventions = reverse("interventions-autocomplete") - cls.atcmplt_code_comp_action = reverse("codes-compensation-action-autocomplete") - cls.atcmplt_code_comp_biotope = reverse("codes-biotope-autocomplete") - cls.atcmplt_code_comp_law = reverse("codes-law-autocomplete") - cls.atcmplt_code_comp_process = reverse("codes-process-type-autocomplete") - cls.atcmplt_code_comp_reg_off = reverse("codes-registration-office-autocomplete") - cls.atcmplt_code_comp_cons_off = reverse("codes-conservation-office-autocomplete") - cls.atcmplt_code_share_user = reverse("share-user-autocomplete") + cls.atcmplt_accs = reverse("compensation:acc:autocomplete") + cls.atcmplt_interventions = reverse("intervention:autocomplete") + cls.atcmplt_code_comp_action = reverse("codelist:compensation-action-autocomplete") + cls.atcmplt_code_comp_biotope = reverse("codelist:biotope-autocomplete") + cls.atcmplt_code_comp_law = reverse("codelist:law-autocomplete") + cls.atcmplt_code_comp_process = reverse("codelist:process-type-autocomplete") + cls.atcmplt_code_comp_reg_off = reverse("codelist:registration-office-autocomplete") + cls.atcmplt_code_comp_cons_off = reverse("codelist:conservation-office-autocomplete") + cls.atcmplt_code_share_user = reverse("user:share-user-autocomplete") def _test_views_anonymous_user(self): # ATTENTION: As of the current state of django-autocomplete-light, there is no way to check on authenticated diff --git a/konova/urls.py b/konova/urls.py index e0126832..1909091e 100644 --- a/konova/urls.py +++ b/konova/urls.py @@ -17,11 +17,6 @@ import debug_toolbar from django.contrib import admin from django.urls import path, include -from konova.autocompletes import EcoAccountAutocomplete, \ - InterventionAutocomplete, CompensationActionCodeAutocomplete, BiotopeCodeAutocomplete, LawCodeAutocomplete, \ - RegistrationOfficeCodeAutocomplete, ConservationOfficeCodeAutocomplete, ProcessTypeCodeAutocomplete, \ - ShareUserAutocomplete, BiotopeExtraCodeAutocomplete, CompensationActionDetailCodeAutocomplete, \ - ShareTeamAutocomplete, HandlerCodeAutocomplete, TeamAdminAutocomplete from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG from konova.sso.sso import KonovaSSOClient from konova.views import logout_view, home_view, get_geom_parcels, get_geom_parcels_content, map_client_proxy_view @@ -43,22 +38,6 @@ urlpatterns = [ path('geom//parcels/', get_geom_parcels, name="geometry-parcels"), path('geom//parcels/', get_geom_parcels_content, name="geometry-parcels-content"), path('client/proxy', map_client_proxy_view, name="map-client-proxy"), - - # Autocomplete paths for all apps - path("atcmplt/eco-accounts", EcoAccountAutocomplete.as_view(), name="accounts-autocomplete"), - path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="interventions-autocomplete"), - path("atcmplt/codes/comp/action", CompensationActionCodeAutocomplete.as_view(), name="codes-compensation-action-autocomplete"), - path("atcmplt/codes/comp/action/detail", CompensationActionDetailCodeAutocomplete.as_view(), name="codes-compensation-action-detail-autocomplete"), - path("atcmplt/codes/biotope", BiotopeCodeAutocomplete.as_view(), name="codes-biotope-autocomplete"), - path("atcmplt/codes/biotope/extra", BiotopeExtraCodeAutocomplete.as_view(), name="codes-biotope-extra-type-autocomplete"), - path("atcmplt/codes/law", LawCodeAutocomplete.as_view(), name="codes-law-autocomplete"), - path("atcmplt/codes/prc-type", ProcessTypeCodeAutocomplete.as_view(), name="codes-process-type-autocomplete"), - path("atcmplt/codes/reg-off", RegistrationOfficeCodeAutocomplete.as_view(), name="codes-registration-office-autocomplete"), - path("atcmplt/codes/cons-off", ConservationOfficeCodeAutocomplete.as_view(), name="codes-conservation-office-autocomplete"), - path("atcmplt/codes/handler", HandlerCodeAutocomplete.as_view(), name="codes-handler-autocomplete"), - path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"), - path("atcmplt/share/t", ShareTeamAutocomplete.as_view(), name="share-team-autocomplete"), - path("atcmplt/team/admin", TeamAdminAutocomplete.as_view(), name="team-admin-autocomplete"), ] if DEBUG: diff --git a/user/autocomplete/__init__.py b/user/autocomplete/__init__.py new file mode 100644 index 00000000..ca978536 --- /dev/null +++ b/user/autocomplete/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" diff --git a/user/autocomplete/share.py b/user/autocomplete/share.py new file mode 100644 index 00000000..de634111 --- /dev/null +++ b/user/autocomplete/share.py @@ -0,0 +1,52 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from dal_select2.views import Select2QuerySetView +from django.db.models import Q + +from user.models import User, Team + + +class ShareUserAutocomplete(Select2QuerySetView): + """ Autocomplete for share with single users + + + """ + def get_queryset(self): + if self.request.user.is_anonymous: + return User.objects.none() + qs = User.objects.all() + if self.q: + # Due to privacy concerns only a full username match will return the proper user entry + qs = qs.filter( + Q(username=self.q) | + Q(email=self.q) + ).distinct() + qs = qs.order_by("username") + return qs + + +class ShareTeamAutocomplete(Select2QuerySetView): + """ Autocomplete for share with teams + + """ + def get_queryset(self): + if self.request.user.is_anonymous: + return Team.objects.none() + qs = Team.objects.filter( + deleted__isnull=True + ) + if self.q: + # Due to privacy concerns only a full username match will return the proper user entry + qs = qs.filter( + name__icontains=self.q + ) + qs = qs.order_by( + "name" + ) + return qs + diff --git a/user/autocomplete/team.py b/user/autocomplete/team.py new file mode 100644 index 00000000..377c693e --- /dev/null +++ b/user/autocomplete/team.py @@ -0,0 +1,34 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from dal_select2.views import Select2QuerySetView + +from user.models import User + + +class TeamAdminAutocomplete(Select2QuerySetView): + """ Autocomplete for share with teams + + """ + def get_queryset(self): + if self.request.user.is_anonymous: + return User.objects.none() + qs = User.objects.filter( + id__in=self.forwarded.get("members", []) + ).exclude( + id__in=self.forwarded.get("admins", []) + ) + if self.q: + # Due to privacy concerns only a full username match will return the proper user entry + qs = qs.filter( + name__icontains=self.q + ) + qs = qs.order_by( + "username" + ) + return qs + diff --git a/user/forms/modals/team.py b/user/forms/modals/team.py index d63a2306..b9f3b55b 100644 --- a/user/forms/modals/team.py +++ b/user/forms/modals/team.py @@ -43,7 +43,7 @@ class NewTeamModalForm(BaseModalForm): required=True, queryset=User.objects.all(), widget=autocomplete.ModelSelect2Multiple( - url="share-user-autocomplete", + url="user:share-user-autocomplete", attrs={ "data-placeholder": _("Click for selection"), "data-minimum-input-length": 3, @@ -103,7 +103,7 @@ class EditTeamModalForm(NewTeamModalForm): required=True, queryset=User.objects.all(), widget=autocomplete.ModelSelect2Multiple( - url="team-admin-autocomplete", + url="user:team-admin-autocomplete", forward=[ "members", "admins", diff --git a/user/urls.py b/user/urls.py index f1fb4e8f..ed975488 100644 --- a/user/urls.py +++ b/user/urls.py @@ -7,6 +7,8 @@ Created on: 08.07.21 """ from django.urls import path +from user.autocomplete.share import ShareUserAutocomplete, ShareTeamAutocomplete +from user.autocomplete.team import TeamAdminAutocomplete from user.views import * app_name = "user" @@ -22,4 +24,8 @@ urlpatterns = [ path("team//remove", remove_team_view, name="team-remove"), path("team//leave", leave_team_view, name="team-leave"), + # Autocomplete urls + path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"), + path("atcmplt/share/t", ShareTeamAutocomplete.as_view(), name="share-team-autocomplete"), + path("atcmplt/team/admin", TeamAdminAutocomplete.as_view(), name="team-admin-autocomplete"), ] \ No newline at end of file From 75777d65c6d30777d4340f01f35ab5c22b36eaf4 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 18 Aug 2022 13:19:04 +0200 Subject: [PATCH 17/48] Filters refactoring * splits filters.py of compensation and ema app into separate files in new /filters module * optimizes entry search for multi keyword input --- compensation/filters/__init__.py | 7 ++ .../{filters.py => filters/compensation.py} | 79 +----------------- compensation/filters/eco_account.py | 82 +++++++++++++++++++ compensation/tables/compensation.py | 2 +- compensation/tables/eco_account.py | 3 +- ema/filters.py | 2 +- konova/filters/mixins/keyword.py | 5 +- 7 files changed, 101 insertions(+), 79 deletions(-) create mode 100644 compensation/filters/__init__.py rename compensation/{filters.py => filters/compensation.py} (59%) create mode 100644 compensation/filters/eco_account.py diff --git a/compensation/filters/__init__.py b/compensation/filters/__init__.py new file mode 100644 index 00000000..ca978536 --- /dev/null +++ b/compensation/filters/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" diff --git a/compensation/filters.py b/compensation/filters/compensation.py similarity index 59% rename from compensation/filters.py rename to compensation/filters/compensation.py index 186a7214..aa1c967b 100644 --- a/compensation/filters.py +++ b/compensation/filters/compensation.py @@ -1,17 +1,14 @@ """ Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 29.07.21 +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 """ -import django_filters -from django.utils.translation import gettext_lazy as _ -from django import forms from django.db.models import QuerySet, Q -from konova.filters.mixins.office import ConservationOfficeTableFilterMixin -from konova.filters.table_filters import QueryTableFilter, CheckboxTableFilter, SelectionTableFilter, AbstractTableFilter +from konova.filters.table_filters import AbstractTableFilter, CheckboxTableFilter, QueryTableFilter, \ + SelectionTableFilter class SelectionCompensationTableFilter(SelectionTableFilter): @@ -114,71 +111,3 @@ class CompensationTableFilter(AbstractTableFilter): ) # Overwrite final queryset as well self.qs = self.checkbox_filter.qs - - -class CheckboxEcoAccountTableFilter(CheckboxTableFilter): - sr = django_filters.BooleanFilter( - method='filter_only_show_unrecorded', - label=_("Show only unrecorded"), - label_suffix=_(""), - widget=forms.CheckboxInput( - attrs={ - "class": "form-check-input", - } - ) - ) - - def filter_only_show_unrecorded(self, queryset, name, value) -> QuerySet: - """ Filters queryset depending on value of 'show_recorded' setting - - Args: - queryset (): - name (): - value (): - - Returns: - - """ - if value: - return queryset.filter( - recorded=None, - ) - else: - return queryset - - -class SelectionEcoAccountTableFilter(ConservationOfficeTableFilterMixin): - """ Special selection table filter for eco accounts - - EcoAccounts only need a selection filter for conservation office - - """ - pass - - -class EcoAccountTableFilter(AbstractTableFilter): - """ TableFilter for eco accounts - - """ - def __init__(self, user=None, *args, **kwargs): - super().__init__(*args, **kwargs) - self.user = user - qs = kwargs.get("queryset", None) - request_data = kwargs.get("data", None) - - # Pipe the queryset through all needed filters - self.selection_filter = SelectionEcoAccountTableFilter( - data=request_data, - queryset=qs, - ) - self.query_filter = QueryTableFilter( - data=request_data, - queryset=self.selection_filter.qs, - ) - self.checkbox_filter = CheckboxEcoAccountTableFilter( - user=user, - data=request_data, - queryset=self.query_filter.qs, - ) - # Overwrite the final queryset result - self.qs = self.checkbox_filter.qs diff --git a/compensation/filters/eco_account.py b/compensation/filters/eco_account.py new file mode 100644 index 00000000..b71e38d8 --- /dev/null +++ b/compensation/filters/eco_account.py @@ -0,0 +1,82 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 18.08.22 + +""" +from django import forms +from django.db.models import QuerySet +from django.utils.translation import gettext_lazy as _ +import django_filters + +from konova.filters.mixins.office import ConservationOfficeTableFilterMixin +from konova.filters.table_filters import AbstractTableFilter, CheckboxTableFilter, QueryTableFilter + + +class CheckboxEcoAccountTableFilter(CheckboxTableFilter): + sr = django_filters.BooleanFilter( + method='filter_only_show_unrecorded', + label=_("Show only unrecorded"), + label_suffix=_(""), + widget=forms.CheckboxInput( + attrs={ + "class": "form-check-input", + } + ) + ) + + def filter_only_show_unrecorded(self, queryset, name, value) -> QuerySet: + """ Filters queryset depending on value of 'show_recorded' setting + + Args: + queryset (): + name (): + value (): + + Returns: + + """ + if value: + return queryset.filter( + recorded=None, + ) + else: + return queryset + + +class SelectionEcoAccountTableFilter(ConservationOfficeTableFilterMixin): + """ Special selection table filter for eco accounts + + EcoAccounts only need a selection filter for conservation office + + """ + pass + + +class EcoAccountTableFilter(AbstractTableFilter): + """ TableFilter for eco accounts + + """ + def __init__(self, user=None, *args, **kwargs): + super().__init__(*args, **kwargs) + self.user = user + qs = kwargs.get("queryset", None) + request_data = kwargs.get("data", None) + + # Pipe the queryset through all needed filters + self.selection_filter = SelectionEcoAccountTableFilter( + data=request_data, + queryset=qs, + ) + self.query_filter = QueryTableFilter( + data=request_data, + queryset=self.selection_filter.qs, + ) + self.checkbox_filter = CheckboxEcoAccountTableFilter( + user=user, + data=request_data, + queryset=self.query_filter.qs, + ) + # Overwrite the final queryset result + self.qs = self.checkbox_filter.qs diff --git a/compensation/tables/compensation.py b/compensation/tables/compensation.py index 92fe1840..032ab67e 100644 --- a/compensation/tables/compensation.py +++ b/compensation/tables/compensation.py @@ -11,7 +11,7 @@ from django.urls import reverse from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ -from compensation.filters import CompensationTableFilter +from compensation.filters.compensation import CompensationTableFilter from compensation.models import Compensation from konova.utils.message_templates import DATA_IS_UNCHECKED, DATA_CHECKED_ON_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE from konova.utils.tables import BaseTable, TableRenderMixin diff --git a/compensation/tables/eco_account.py b/compensation/tables/eco_account.py index 0cf4a4ef..17b8ecaa 100644 --- a/compensation/tables/eco_account.py +++ b/compensation/tables/eco_account.py @@ -11,12 +11,13 @@ from django.urls import reverse from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ -from compensation.filters import EcoAccountTableFilter +from compensation.filters.eco_account import EcoAccountTableFilter from compensation.models import EcoAccount from konova.utils.tables import TableRenderMixin, BaseTable import django_tables2 as tables + class EcoAccountTable(BaseTable, TableRenderMixin): id = tables.Column( verbose_name=_("Identifier"), diff --git a/ema/filters.py b/ema/filters.py index 70b93da0..b976a4c9 100644 --- a/ema/filters.py +++ b/ema/filters.py @@ -5,7 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 19.08.21 """ -from compensation.filters import EcoAccountTableFilter +from compensation.filters.eco_account import EcoAccountTableFilter class EmaTableFilter(EcoAccountTableFilter): diff --git a/konova/filters/mixins/keyword.py b/konova/filters/mixins/keyword.py index 212599c8..19bca65f 100644 --- a/konova/filters/mixins/keyword.py +++ b/konova/filters/mixins/keyword.py @@ -36,6 +36,9 @@ class KeywordTableFilterMixin(django_filters.FilterSet): """ value = value.strip() + value = value.split(" ") # build filter expression - q = Q(title__icontains=value) | Q(identifier__icontains=value) + q = Q() + for val in value: + q &= Q(title__icontains=val) | Q(identifier__icontains=val) return queryset.filter(q) From 55a69d45c310ebe6b4d3c4fb02e8491009dbf8ac Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 19 Aug 2022 07:34:09 +0200 Subject: [PATCH 18/48] Intervention views * splits intervention/views.py (+700 lines) into separate files in new module * view files can now be found in /intervention/views/... --- intervention/urls.py | 19 +- intervention/views.py | 756 ----------------------------- intervention/views/__init__.py | 7 + intervention/views/check.py | 39 ++ intervention/views/compensation.py | 45 ++ intervention/views/deduction.py | 97 ++++ intervention/views/document.py | 112 +++++ intervention/views/intervention.py | 251 ++++++++++ intervention/views/log.py | 41 ++ intervention/views/record.py | 39 ++ intervention/views/report.py | 73 +++ intervention/views/resubmission.py | 39 ++ intervention/views/revocation.py | 117 +++++ intervention/views/share.py | 78 +++ 14 files changed, 951 insertions(+), 762 deletions(-) delete mode 100644 intervention/views.py create mode 100644 intervention/views/__init__.py create mode 100644 intervention/views/check.py create mode 100644 intervention/views/compensation.py create mode 100644 intervention/views/deduction.py create mode 100644 intervention/views/document.py create mode 100644 intervention/views/intervention.py create mode 100644 intervention/views/log.py create mode 100644 intervention/views/record.py create mode 100644 intervention/views/report.py create mode 100644 intervention/views/resubmission.py create mode 100644 intervention/views/revocation.py create mode 100644 intervention/views/share.py diff --git a/intervention/urls.py b/intervention/urls.py index b3cc7038..2d0dfdb5 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -8,11 +8,18 @@ Created on: 30.11.20 from django.urls import path from intervention.autocomplete.intervention import InterventionAutocomplete -from intervention.views import index_view, new_view, detail_view, edit_view, remove_view, new_document_view, share_view, \ - create_share_view, remove_revocation_view, new_revocation_view, check_view, log_view, new_deduction_view, \ - record_view, remove_document_view, get_document_view, get_revocation_view, new_id_view, report_view, \ - remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view, \ - create_resubmission_view +from intervention.views.check import check_view +from intervention.views.compensation import remove_compensation_view +from intervention.views.deduction import new_deduction_view, edit_deduction_view, remove_deduction_view +from intervention.views.document import new_document_view, get_document_view, remove_document_view, edit_document_view +from intervention.views.intervention import index_view, new_view, new_id_view, detail_view, edit_view, remove_view +from intervention.views.log import log_view +from intervention.views.record import record_view +from intervention.views.report import report_view +from intervention.views.resubmission import create_resubmission_view +from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \ + get_revocation_view +from intervention.views.share import share_view, create_share_view app_name = "intervention" urlpatterns = [ @@ -52,4 +59,4 @@ urlpatterns = [ # Autocomplete path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="autocomplete"), -] \ No newline at end of file +] diff --git a/intervention/views.py b/intervention/views.py deleted file mode 100644 index e622488f..00000000 --- a/intervention/views.py +++ /dev/null @@ -1,756 +0,0 @@ -from django.contrib.auth.decorators import login_required -from django.core.exceptions import ObjectDoesNotExist -from django.utils.translation import gettext_lazy as _ -from django.http import HttpRequest, JsonResponse, Http404 -from django.shortcuts import render - -from intervention.forms.intervention import NewInterventionForm, EditInterventionForm -from intervention.forms.modals.check import CheckModalForm -from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, RemoveEcoAccountDeductionModalForm, \ - EditEcoAccountDeductionModalForm -from intervention.forms.modals.document import NewInterventionDocumentModalForm -from intervention.forms.modals.revocation import EditRevocationModalForm, RemoveRevocationModalForm, \ - NewRevocationModalForm -from intervention.forms.modals.share import ShareModalForm -from intervention.models import Intervention, Revocation, InterventionDocument, RevocationDocument -from intervention.tables import InterventionTable -from konova.contexts import BaseContext -from konova.decorators import * -from konova.forms import SimpleGeomForm -from konova.forms.modals import RemoveModalForm, RecordModalForm, EditDocumentModalForm, ResubmissionModalForm -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.documents import remove_document, get_document -from konova.utils.generators import generate_qr_code -from konova.utils.message_templates import INTERVENTION_INVALID, FORM_INVALID, IDENTIFIER_REPLACED, \ - CHECKED_RECORDED_RESET, DEDUCTION_REMOVED, DEDUCTION_ADDED, REVOCATION_ADDED, REVOCATION_REMOVED, \ - COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, DEDUCTION_EDITED, REVOCATION_EDITED, DOCUMENT_EDITED, \ - RECORDED_BLOCKS_EDIT, DATA_CHECKED_PREVIOUSLY_TEMPLATE -from konova.utils.user_checks import in_group - - -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ - Renders the index view for Interventions - - Args: - request (HttpRequest): The incoming request - - Returns: - A rendered view - """ - template = "generic_index.html" - - # Filtering by user access is performed in table filter inside of InterventionTableFilter class - interventions = Intervention.objects.filter( - deleted=None, # not deleted - ).select_related( - "legal" - ) - table = InterventionTable( - request=request, - queryset=interventions - ) - context = { - "table": table, - TAB_TITLE_IDENTIFIER: _("Interventions - Overview"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -def new_view(request: HttpRequest): - """ - Renders a view for a new intervention creation - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "intervention/form/view.html" - data_form = NewInterventionForm(request.POST or None) - geom_form = SimpleGeomForm(request.POST or None, read_only=False) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - generated_identifier = data_form.cleaned_data.get("identifier", None) - intervention = data_form.save(request.user, geom_form) - if generated_identifier != intervention.identifier: - messages.info( - request, - IDENTIFIER_REPLACED.format( - generated_identifier, - intervention.identifier - ) - ) - messages.success(request, _("Intervention {} added").format(intervention.identifier)) - return redirect("intervention:detail", id=intervention.id) - else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("New intervention"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -def new_id_view(request: HttpRequest): - """ JSON endpoint - - Provides fetching of free identifiers for e.g. AJAX calls - - """ - tmp_intervention = Intervention() - identifier = tmp_intervention.generate_new_identifier() - while Intervention.objects.filter(identifier=identifier).exists(): - identifier = tmp_intervention.generate_new_identifier() - return JsonResponse( - data={ - "gen_data": identifier - } - ) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def new_document_view(request: HttpRequest, id: str): - """ Renders a form for uploading new documents - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id to which the new document will be related - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - form = NewInterventionDocumentModalForm(request.POST or None, request.FILES or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=DOCUMENT_ADDED, - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -def get_revocation_view(request: HttpRequest, doc_id: str): - """ Returns the revocation document as downloadable file - - Wraps the generic document fetcher function from konova.utils. - - Args: - request (HttpRequest): The incoming request - doc_id (str): The document id - - Returns: - - """ - doc = get_object_or_404(RevocationDocument, id=doc_id) - # File download only possible if related instance is shared with user - if not doc.instance.legal.intervention.users.filter(id=request.user.id): - messages.info( - request, - DATA_UNSHARED - ) - return redirect("intervention:detail", id=doc.instance.id) - return get_document(doc) - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def get_document_view(request: HttpRequest, id: str, doc_id: str): - """ Returns the document as downloadable file - - Wraps the generic document fetcher function from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The intervention id - doc_id (str): The document id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - doc = get_object_or_404(InterventionDocument, id=doc_id) - return get_document(doc) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def remove_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The intervention id - doc_id (str): The document id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - doc = get_object_or_404(InterventionDocument, id=doc_id) - return remove_document( - request, - doc - ) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def edit_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The intervention id - doc_id (str): The document id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - doc = get_object_or_404(InterventionDocument, id=doc_id) - form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=intervention, document=doc, request=request) - return form.process_request( - request, - DOCUMENT_EDITED, - redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" - ) - - -@login_required -@any_group_check -def detail_view(request: HttpRequest, id: str): - """ Renders a detail view for viewing an intervention's data - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - - Returns: - - """ - template = "intervention/detail/view.html" - - # Fetch data, filter out deleted related data - intervention = get_object_or_404( - Intervention.objects.select_related( - "geometry", - "legal", - "responsible", - ), - id=id - ) - compensations = intervention.compensations.filter( - deleted=None, - ) - _user = request.user - is_data_shared = intervention.is_shared_with(user=_user) - - geom_form = SimpleGeomForm( - instance=intervention, - ) - last_checked = intervention.get_last_checked_action() - last_checked_tooltip = "" - if last_checked: - last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user) - - context = { - "obj": intervention, - "last_checked": last_checked, - "last_checked_tooltip": last_checked_tooltip, - "compensations": compensations, - "has_access": is_data_shared, - "geom_form": geom_form, - "is_default_member": in_group(_user, DEFAULT_GROUP), - "is_zb_member": in_group(_user, ZB_GROUP), - "is_ets_member": in_group(_user, ETS_GROUP), - "LANIS_LINK": intervention.get_LANIS_link(), - TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}", - } - - request = intervention.set_status_messages(request) - - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing interventions - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "intervention/form/view.html" - # Get object from db - intervention = get_object_or_404(Intervention, id=id) - if intervention.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("intervention:detail", id=id) - - # Create forms, initialize with values from db/from POST request - data_form = EditInterventionForm(request.POST or None, instance=intervention) - geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - # The data form takes the geom form for processing, as well as the performing user - # Save the current state of recorded|checked to inform the user in case of a status reset due to editing - i_rec = intervention.recorded is not None - i_check = intervention.checked is not None - intervention = data_form.save(request.user, geom_form) - messages.success(request, _("Intervention {} edited").format(intervention.identifier)) - if i_check or i_rec: - messages.info(request, CHECKED_RECORDED_RESET) - return redirect("intervention:detail", id=intervention.id) - else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def remove_view(request: HttpRequest, id: str): - """ Renders a remove view for this intervention - - Args: - request (HttpRequest): The incoming request - id (str): The uuid id as string - - Returns: - - """ - obj = Intervention.objects.get(id=id) - identifier = obj.identifier - form = RemoveModalForm(request.POST or None, instance=obj, request=request) - return form.process_request( - request, - _("{} removed").format(identifier), - redirect_url=reverse("intervention:index") - ) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def edit_revocation_view(request: HttpRequest, id: str, revocation_id: str): - """ Renders a edit view for a revocation - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id as string - revocation_id (str): The revocation's id as string - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - revocation = get_object_or_404(Revocation, id=revocation_id) - - form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, revocation=revocation, request=request) - return form.process_request( - request, - REVOCATION_EDITED, - redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def remove_revocation_view(request: HttpRequest, id: str, revocation_id: str): - """ Renders a remove view for a revocation - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id as string - revocation_id (str): The revocation's id as string - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - revocation = get_object_or_404(Revocation, id=revocation_id) - - form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation, request=request) - return form.process_request( - request, - REVOCATION_REMOVED, - redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" - ) - - -@login_required -def share_view(request: HttpRequest, id: str, token: str): - """ Performs sharing of an intervention - - If token given in url is not valid, the user will be redirected to the dashboard - - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id - token (str): Access token for intervention - - Returns: - - """ - user = request.user - intervention = get_object_or_404(Intervention, id=id) - # Check tokens - if intervention.access_token == token: - # Send different messages in case user has already been added to list of sharing users - if intervention.is_shared_with(user): - messages.info( - request, - _("{} has already been shared with you").format(intervention.identifier) - ) - else: - messages.success( - request, - _("{} has been shared with you").format(intervention.identifier) - ) - intervention.share_with_user(user) - return redirect("intervention:detail", id=id) - else: - messages.error( - request, - _("Share link invalid"), - extra_tags="danger", - ) - return redirect("home") - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def create_share_view(request: HttpRequest, id: str): - """ Renders sharing form for an intervention - - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - form = ShareModalForm(request.POST or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=_("Share settings updated") - ) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def create_resubmission_view(request: HttpRequest, id: str): - """ Renders resubmission form for an intervention - - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - form = ResubmissionModalForm(request.POST or None, instance=intervention, request=request) - form.action_url = reverse("intervention:resubmission-create", args=(id,)) - return form.process_request( - request, - msg_success=_("Resubmission set"), - redirect_url=reverse("intervention:detail", args=(id,)) - ) - - -@login_required -@registration_office_group_required -@shared_access_required(Intervention, "id") -def check_view(request: HttpRequest, id: str): - """ Renders check form for an intervention - - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - form = CheckModalForm(request.POST or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=_("Check performed"), - msg_error=INTERVENTION_INVALID - ) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def new_revocation_view(request: HttpRequest, id: str): - """ Renders sharing form for an intervention - - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=REVOCATION_ADDED, - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def log_view(request: HttpRequest, id: str): - """ Renders a log view using modal - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - template = "modal/modal_generic.html" - body_template = "log.html" - - context = { - "modal_body_template": body_template, - "log": intervention.log.all(), - "modal_title": _("Log"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def new_deduction_view(request: HttpRequest, id: str): - """ Renders a modal form view for creating deductions - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id which shall benefit from this deduction - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - form = NewEcoAccountDeductionModalForm(request.POST or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=DEDUCTION_ADDED, - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data", - ) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def remove_deduction_view(request: HttpRequest, id: str, deduction_id: str): - """ Renders a modal view for removing deductions - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - deduction_id (str): The deduction's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - try: - eco_deduction = intervention.deductions.get(id=deduction_id) - except ObjectDoesNotExist: - raise Http404("Unknown deduction") - - form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=intervention, deduction=eco_deduction, request=request) - return form.process_request( - request=request, - msg_success=DEDUCTION_REMOVED, - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def edit_deduction_view(request: HttpRequest, id: str, deduction_id: str): - """ Renders a modal view for removing deductions - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - deduction_id (str): The deduction's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - try: - eco_deduction = intervention.deductions.get(id=deduction_id) - except ObjectDoesNotExist: - raise Http404("Unknown deduction") - - form = EditEcoAccountDeductionModalForm(request.POST or None, instance=intervention, deduction=eco_deduction, request=request) - return form.process_request( - request=request, - msg_success=DEDUCTION_EDITED, - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Intervention, "id") -def record_view(request: HttpRequest, id: str): - """ Renders a modal form for recording an intervention - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - form = RecordModalForm(request.POST or None, instance=intervention, request=request) - msg_succ = _("{} unrecorded") if intervention.recorded else _("{} recorded") - msg_succ = msg_succ.format(intervention.identifier) - return form.process_request( - request, - msg_succ, - msg_error=_("There are errors on this intervention:") - ) - - -def remove_compensation_view(request:HttpRequest, id: str, comp_id: str): - """ Renders a modal view for removing the compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - try: - comp = intervention.compensations.get( - id=comp_id - ) - except ObjectDoesNotExist: - raise Http404("Unknown compensation") - form = RemoveModalForm(request.POST or None, instance=comp, request=request) - return form.process_request( - request=request, - msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier), - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data", - ) - - -def report_view(request:HttpRequest, id: str): - """ Renders the public report view - - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention - - Returns: - - """ - template = "intervention/report/report.html" - intervention = get_object_or_404(Intervention, id=id) - - tab_title = _("Report {}").format(intervention.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not intervention.is_ready_for_publish(): - template = "report/unavailable.html" - context = { - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) - - # Prepare data for map viewer - geom_form = SimpleGeomForm( - instance=intervention - ) - parcels = intervention.get_underlying_parcels() - - distinct_deductions = intervention.deductions.all().distinct( - "account" - ) - qrcode_url = request.build_absolute_uri(reverse("intervention:report", args=(id,))) - qrcode_img = generate_qr_code(qrcode_url, 10) - qrcode_lanis_url = intervention.get_LANIS_link() - qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) - - context = { - "obj": intervention, - "deductions": distinct_deductions, - "qrcode": { - "img": qrcode_img, - "url": qrcode_url, - }, - "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url, - }, - "geom_form": geom_form, - "parcels": parcels, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) diff --git a/intervention/views/__init__.py b/intervention/views/__init__.py new file mode 100644 index 00000000..3a996e1f --- /dev/null +++ b/intervention/views/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" diff --git a/intervention/views/check.py b/intervention/views/check.py new file mode 100644 index 00000000..1fae75bb --- /dev/null +++ b/intervention/views/check.py @@ -0,0 +1,39 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext_lazy as _ + +from intervention.forms.modals.check import CheckModalForm +from intervention.models import Intervention +from konova.decorators import registration_office_group_required, shared_access_required +from konova.utils.message_templates import INTERVENTION_INVALID + + +@login_required +@registration_office_group_required +@shared_access_required(Intervention, "id") +def check_view(request: HttpRequest, id: str): + """ Renders check form for an intervention + + Args: + request (HttpRequest): The incoming request + id (str): Intervention's id + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + form = CheckModalForm(request.POST or None, instance=intervention, request=request) + return form.process_request( + request, + msg_success=_("Check performed"), + msg_error=INTERVENTION_INVALID + ) + diff --git a/intervention/views/compensation.py b/intervention/views/compensation.py new file mode 100644 index 00000000..94e8dfd5 --- /dev/null +++ b/intervention/views/compensation.py @@ -0,0 +1,45 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.core.exceptions import ObjectDoesNotExist +from django.http import HttpRequest, Http404 +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from intervention.models import Intervention +from konova.decorators import shared_access_required +from konova.forms.modals import RemoveModalForm +from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE + + +@login_required +@shared_access_required(Intervention, "id") +def remove_compensation_view(request: HttpRequest, id: str, comp_id: str): + """ Renders a modal view for removing the compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + try: + comp = intervention.compensations.get( + id=comp_id + ) + except ObjectDoesNotExist: + raise Http404("Unknown compensation") + form = RemoveModalForm(request.POST or None, instance=comp, request=request) + return form.process_request( + request=request, + msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier), + redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data", + ) + diff --git a/intervention/views/deduction.py b/intervention/views/deduction.py new file mode 100644 index 00000000..2fb841c1 --- /dev/null +++ b/intervention/views/deduction.py @@ -0,0 +1,97 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.core.exceptions import ObjectDoesNotExist +from django.http import HttpRequest, Http404 +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, RemoveEcoAccountDeductionModalForm, \ + EditEcoAccountDeductionModalForm +from intervention.models import Intervention +from konova.decorators import default_group_required, shared_access_required +from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def new_deduction_view(request: HttpRequest, id: str): + """ Renders a modal form view for creating deductions + + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id which shall benefit from this deduction + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + form = NewEcoAccountDeductionModalForm(request.POST or None, instance=intervention, request=request) + return form.process_request( + request, + msg_success=DEDUCTION_ADDED, + redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data", + ) + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def remove_deduction_view(request: HttpRequest, id: str, deduction_id: str): + """ Renders a modal view for removing deductions + + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id + deduction_id (str): The deduction's id + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + try: + eco_deduction = intervention.deductions.get(id=deduction_id) + except ObjectDoesNotExist: + raise Http404("Unknown deduction") + + form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=intervention, deduction=eco_deduction, request=request) + return form.process_request( + request=request, + msg_success=DEDUCTION_REMOVED, + redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def edit_deduction_view(request: HttpRequest, id: str, deduction_id: str): + """ Renders a modal view for removing deductions + + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id + deduction_id (str): The deduction's id + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + try: + eco_deduction = intervention.deductions.get(id=deduction_id) + except ObjectDoesNotExist: + raise Http404("Unknown deduction") + + form = EditEcoAccountDeductionModalForm(request.POST or None, instance=intervention, deduction=eco_deduction, request=request) + return form.process_request( + request=request, + msg_success=DEDUCTION_EDITED, + redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" + ) + diff --git a/intervention/views/document.py b/intervention/views/document.py new file mode 100644 index 00000000..be43fbd5 --- /dev/null +++ b/intervention/views/document.py @@ -0,0 +1,112 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from intervention.forms.modals.document import NewInterventionDocumentModalForm +from intervention.models import Intervention, InterventionDocument +from konova.decorators import default_group_required, shared_access_required +from konova.forms.modals import EditDocumentModalForm +from konova.utils.documents import get_document, remove_document +from konova.utils.message_templates import DOCUMENT_ADDED, DOCUMENT_EDITED + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def new_document_view(request: HttpRequest, id: str): + """ Renders a form for uploading new documents + + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id to which the new document will be related + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + form = NewInterventionDocumentModalForm(request.POST or None, request.FILES or None, instance=intervention, request=request) + return form.process_request( + request, + msg_success=DOCUMENT_ADDED, + redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def get_document_view(request: HttpRequest, id: str, doc_id: str): + """ Returns the document as downloadable file + + Wraps the generic document fetcher function from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The intervention id + doc_id (str): The document id + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + doc = get_object_or_404(InterventionDocument, id=doc_id) + return get_document(doc) + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def remove_document_view(request: HttpRequest, id: str, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The intervention id + doc_id (str): The document id + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + doc = get_object_or_404(InterventionDocument, id=doc_id) + return remove_document( + request, + doc + ) + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def edit_document_view(request: HttpRequest, id: str, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The intervention id + doc_id (str): The document id + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + doc = get_object_or_404(InterventionDocument, id=doc_id) + form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=intervention, document=doc, request=request) + return form.process_request( + request, + DOCUMENT_EDITED, + redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" + ) + diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py new file mode 100644 index 00000000..bf33bfb9 --- /dev/null +++ b/intervention/views/intervention.py @@ -0,0 +1,251 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.http import JsonResponse, HttpRequest +from django.shortcuts import get_object_or_404, render, redirect +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from intervention.forms.intervention import EditInterventionForm, NewInterventionForm +from intervention.models import Intervention +from intervention.tables import InterventionTable +from konova.contexts import BaseContext +from konova.decorators import default_group_required, shared_access_required, any_group_check +from konova.forms import SimpleGeomForm +from konova.forms.modals import RemoveModalForm +from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ + CHECKED_RECORDED_RESET, FORM_INVALID, IDENTIFIER_REPLACED +from konova.utils.user_checks import in_group + + +@login_required +@any_group_check +def index_view(request: HttpRequest): + """ + Renders the index view for Interventions + + Args: + request (HttpRequest): The incoming request + + Returns: + A rendered view + """ + template = "generic_index.html" + + # Filtering by user access is performed in table filter inside of InterventionTableFilter class + interventions = Intervention.objects.filter( + deleted=None, # not deleted + ).select_related( + "legal" + ) + table = InterventionTable( + request=request, + queryset=interventions + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: _("Interventions - Overview"), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@default_group_required +def new_view(request: HttpRequest): + """ + Renders a view for a new intervention creation + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + template = "intervention/form/view.html" + data_form = NewInterventionForm(request.POST or None) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) + if request.method == "POST": + if data_form.is_valid() and geom_form.is_valid(): + generated_identifier = data_form.cleaned_data.get("identifier", None) + intervention = data_form.save(request.user, geom_form) + if generated_identifier != intervention.identifier: + messages.info( + request, + IDENTIFIER_REPLACED.format( + generated_identifier, + intervention.identifier + ) + ) + messages.success(request, _("Intervention {} added").format(intervention.identifier)) + return redirect("intervention:detail", id=intervention.id) + else: + messages.error(request, FORM_INVALID, extra_tags="danger",) + else: + # For clarification: nothing in this case + pass + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New intervention"), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@default_group_required +def new_id_view(request: HttpRequest): + """ JSON endpoint + + Provides fetching of free identifiers for e.g. AJAX calls + + """ + tmp_intervention = Intervention() + identifier = tmp_intervention.generate_new_identifier() + while Intervention.objects.filter(identifier=identifier).exists(): + identifier = tmp_intervention.generate_new_identifier() + return JsonResponse( + data={ + "gen_data": identifier + } + ) + + +@login_required +@any_group_check +def detail_view(request: HttpRequest, id: str): + """ Renders a detail view for viewing an intervention's data + + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id + + Returns: + + """ + template = "intervention/detail/view.html" + + # Fetch data, filter out deleted related data + intervention = get_object_or_404( + Intervention.objects.select_related( + "geometry", + "legal", + "responsible", + ), + id=id + ) + compensations = intervention.compensations.filter( + deleted=None, + ) + _user = request.user + is_data_shared = intervention.is_shared_with(user=_user) + + geom_form = SimpleGeomForm( + instance=intervention, + ) + last_checked = intervention.get_last_checked_action() + last_checked_tooltip = "" + if last_checked: + last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user) + + context = { + "obj": intervention, + "last_checked": last_checked, + "last_checked_tooltip": last_checked_tooltip, + "compensations": compensations, + "has_access": is_data_shared, + "geom_form": geom_form, + "is_default_member": in_group(_user, DEFAULT_GROUP), + "is_zb_member": in_group(_user, ZB_GROUP), + "is_ets_member": in_group(_user, ETS_GROUP), + "LANIS_LINK": intervention.get_LANIS_link(), + TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}", + } + + request = intervention.set_status_messages(request) + + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def edit_view(request: HttpRequest, id: str): + """ + Renders a view for editing interventions + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + template = "intervention/form/view.html" + # Get object from db + intervention = get_object_or_404(Intervention, id=id) + if intervention.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("intervention:detail", id=id) + + # Create forms, initialize with values from db/from POST request + data_form = EditInterventionForm(request.POST or None, instance=intervention) + geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention) + if request.method == "POST": + if data_form.is_valid() and geom_form.is_valid(): + # The data form takes the geom form for processing, as well as the performing user + # Save the current state of recorded|checked to inform the user in case of a status reset due to editing + i_rec = intervention.recorded is not None + i_check = intervention.checked is not None + intervention = data_form.save(request.user, geom_form) + messages.success(request, _("Intervention {} edited").format(intervention.identifier)) + if i_check or i_rec: + messages.info(request, CHECKED_RECORDED_RESET) + return redirect("intervention:detail", id=intervention.id) + else: + messages.error(request, FORM_INVALID, extra_tags="danger",) + else: + # For clarification: nothing in this case + pass + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def remove_view(request: HttpRequest, id: str): + """ Renders a remove view for this intervention + + Args: + request (HttpRequest): The incoming request + id (str): The uuid id as string + + Returns: + + """ + obj = Intervention.objects.get(id=id) + identifier = obj.identifier + form = RemoveModalForm(request.POST or None, instance=obj, request=request) + return form.process_request( + request, + _("{} removed").format(identifier), + redirect_url=reverse("intervention:index") + ) diff --git a/intervention/views/log.py b/intervention/views/log.py new file mode 100644 index 00000000..fcb69197 --- /dev/null +++ b/intervention/views/log.py @@ -0,0 +1,41 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404, render +from django.utils.translation import gettext_lazy as _ + +from intervention.models import Intervention +from konova.contexts import BaseContext +from konova.decorators import shared_access_required, default_group_required + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def log_view(request: HttpRequest, id: str): + """ Renders a log view using modal + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + template = "modal/modal_generic.html" + body_template = "log.html" + + context = { + "modal_body_template": body_template, + "log": intervention.log.all(), + "modal_title": _("Log"), + } + context = BaseContext(request, context).context + return render(request, template, context) diff --git a/intervention/views/record.py b/intervention/views/record.py new file mode 100644 index 00000000..c5709b22 --- /dev/null +++ b/intervention/views/record.py @@ -0,0 +1,39 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext_lazy as _ + +from intervention.models import Intervention +from konova.decorators import conservation_office_group_required, shared_access_required +from konova.forms.modals import RecordModalForm + + +@login_required +@conservation_office_group_required +@shared_access_required(Intervention, "id") +def record_view(request: HttpRequest, id: str): + """ Renders a modal form for recording an intervention + + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + form = RecordModalForm(request.POST or None, instance=intervention, request=request) + msg_succ = _("{} unrecorded") if intervention.recorded else _("{} recorded") + msg_succ = msg_succ.format(intervention.identifier) + return form.process_request( + request, + msg_succ, + msg_error=_("There are errors on this intervention:") + ) diff --git a/intervention/views/report.py b/intervention/views/report.py new file mode 100644 index 00000000..fa4798d0 --- /dev/null +++ b/intervention/views/report.py @@ -0,0 +1,73 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.http import HttpRequest +from django.shortcuts import get_object_or_404, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from intervention.models import Intervention +from konova.contexts import BaseContext +from konova.forms import SimpleGeomForm +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.utils.generators import generate_qr_code + + +def report_view(request:HttpRequest, id: str): + """ Renders the public report view + + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention + + Returns: + + """ + template = "intervention/report/report.html" + intervention = get_object_or_404(Intervention, id=id) + + tab_title = _("Report {}").format(intervention.identifier) + # If intervention is not recorded (yet or currently) we need to render another template without any data + if not intervention.is_ready_for_publish(): + template = "report/unavailable.html" + context = { + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) + + # Prepare data for map viewer + geom_form = SimpleGeomForm( + instance=intervention + ) + parcels = intervention.get_underlying_parcels() + + distinct_deductions = intervention.deductions.all().distinct( + "account" + ) + qrcode_url = request.build_absolute_uri(reverse("intervention:report", args=(id,))) + qrcode_img = generate_qr_code(qrcode_url, 10) + qrcode_lanis_url = intervention.get_LANIS_link() + qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) + + context = { + "obj": intervention, + "deductions": distinct_deductions, + "qrcode": { + "img": qrcode_img, + "url": qrcode_url, + }, + "qrcode_lanis": { + "img": qrcode_img_lanis, + "url": qrcode_lanis_url, + }, + "geom_form": geom_form, + "parcels": parcels, + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) diff --git a/intervention/views/resubmission.py b/intervention/views/resubmission.py new file mode 100644 index 00000000..34a618c2 --- /dev/null +++ b/intervention/views/resubmission.py @@ -0,0 +1,39 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from intervention.models import Intervention +from konova.decorators import default_group_required, shared_access_required +from konova.forms.modals import ResubmissionModalForm + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def create_resubmission_view(request: HttpRequest, id: str): + """ Renders resubmission form for an intervention + + Args: + request (HttpRequest): The incoming request + id (str): Intervention's id + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + form = ResubmissionModalForm(request.POST or None, instance=intervention, request=request) + form.action_url = reverse("intervention:resubmission-create", args=(id,)) + return form.process_request( + request, + msg_success=_("Resubmission set"), + redirect_url=reverse("intervention:detail", args=(id,)) + ) diff --git a/intervention/views/revocation.py b/intervention/views/revocation.py new file mode 100644 index 00000000..6c6c70b7 --- /dev/null +++ b/intervention/views/revocation.py @@ -0,0 +1,117 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404, redirect +from django.urls import reverse + +from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \ + RemoveRevocationModalForm +from intervention.models import Intervention, RevocationDocument, Revocation +from konova.decorators import default_group_required, shared_access_required +from konova.utils.documents import get_document +from konova.utils.message_templates import REVOCATION_ADDED, DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def new_revocation_view(request: HttpRequest, id: str): + """ Renders sharing form for an intervention + + Args: + request (HttpRequest): The incoming request + id (str): Intervention's id + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, request=request) + return form.process_request( + request, + msg_success=REVOCATION_ADDED, + redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +def get_revocation_view(request: HttpRequest, doc_id: str): + """ Returns the revocation document as downloadable file + + Wraps the generic document fetcher function from konova.utils. + + Args: + request (HttpRequest): The incoming request + doc_id (str): The document id + + Returns: + + """ + doc = get_object_or_404(RevocationDocument, id=doc_id) + # File download only possible if related instance is shared with user + if not doc.instance.legal.intervention.users.filter(id=request.user.id): + messages.info( + request, + DATA_UNSHARED + ) + return redirect("intervention:detail", id=doc.instance.id) + return get_document(doc) + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def edit_revocation_view(request: HttpRequest, id: str, revocation_id: str): + """ Renders a edit view for a revocation + + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id as string + revocation_id (str): The revocation's id as string + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + revocation = get_object_or_404(Revocation, id=revocation_id) + + form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, revocation=revocation, request=request) + return form.process_request( + request, + REVOCATION_EDITED, + redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def remove_revocation_view(request: HttpRequest, id: str, revocation_id: str): + """ Renders a remove view for a revocation + + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id as string + revocation_id (str): The revocation's id as string + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + revocation = get_object_or_404(Revocation, id=revocation_id) + + form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation, request=request) + return form.process_request( + request, + REVOCATION_REMOVED, + redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" + ) + diff --git a/intervention/views/share.py b/intervention/views/share.py new file mode 100644 index 00000000..ba9cdd3a --- /dev/null +++ b/intervention/views/share.py @@ -0,0 +1,78 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404, redirect +from django.utils.translation import gettext_lazy as _ + +from intervention.forms.modals.share import ShareModalForm +from intervention.models import Intervention +from konova.decorators import default_group_required, shared_access_required + + +@login_required +def share_view(request: HttpRequest, id: str, token: str): + """ Performs sharing of an intervention + + If token given in url is not valid, the user will be redirected to the dashboard + + Args: + request (HttpRequest): The incoming request + id (str): Intervention's id + token (str): Access token for intervention + + Returns: + + """ + user = request.user + intervention = get_object_or_404(Intervention, id=id) + # Check tokens + if intervention.access_token == token: + # Send different messages in case user has already been added to list of sharing users + if intervention.is_shared_with(user): + messages.info( + request, + _("{} has already been shared with you").format(intervention.identifier) + ) + else: + messages.success( + request, + _("{} has been shared with you").format(intervention.identifier) + ) + intervention.share_with_user(user) + return redirect("intervention:detail", id=id) + else: + messages.error( + request, + _("Share link invalid"), + extra_tags="danger", + ) + return redirect("home") + + +@login_required +@default_group_required +@shared_access_required(Intervention, "id") +def create_share_view(request: HttpRequest, id: str): + """ Renders sharing form for an intervention + + Args: + request (HttpRequest): The incoming request + id (str): Intervention's id + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + form = ShareModalForm(request.POST or None, instance=intervention, request=request) + return form.process_request( + request, + msg_success=_("Share settings updated") + ) + From 79840550cb07134454071e6da2665bb5e77dd3fc Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 19 Aug 2022 07:51:49 +0200 Subject: [PATCH 19/48] EMA views * splits ema/views.py (+700 lines) into separate files in new module * view files can now be found in /ema/views/... --- ema/urls.py | 12 +- ema/views.py | 739 -------------------------------------- ema/views/__init__.py | 7 + ema/views/action.py | 89 +++++ ema/views/deadline.py | 90 +++++ ema/views/document.py | 111 ++++++ ema/views/ema.py | 237 ++++++++++++ ema/views/log.py | 41 +++ ema/views/record.py | 37 ++ ema/views/report.py | 79 ++++ ema/views/resubmission.py | 39 ++ ema/views/share.py | 77 ++++ ema/views/state.py | 90 +++++ 13 files changed, 908 insertions(+), 740 deletions(-) delete mode 100644 ema/views.py create mode 100644 ema/views/__init__.py create mode 100644 ema/views/action.py create mode 100644 ema/views/deadline.py create mode 100644 ema/views/document.py create mode 100644 ema/views/ema.py create mode 100644 ema/views/log.py create mode 100644 ema/views/record.py create mode 100644 ema/views/report.py create mode 100644 ema/views/resubmission.py create mode 100644 ema/views/share.py create mode 100644 ema/views/state.py diff --git a/ema/urls.py b/ema/urls.py index 63073d6e..92877271 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -6,7 +6,17 @@ Created on: 19.08.21 """ from django.urls import path -from ema.views import * + +from ema.views.action import action_new_view, action_edit_view, action_remove_view +from ema.views.deadline import deadline_new_view, deadline_edit_view, deadline_remove_view +from ema.views.document import document_new_view, get_document_view, remove_document_view, edit_document_view +from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_view, remove_view +from ema.views.log import log_view +from ema.views.record import record_view +from ema.views.report import report_view +from ema.views.resubmission import create_resubmission_view +from ema.views.share import share_view, create_share_view +from ema.views.state import state_new_view, state_remove_view, state_edit_view app_name = "ema" urlpatterns = [ diff --git a/ema/views.py b/ema/views.py deleted file mode 100644 index 2dd6e51f..00000000 --- a/ema/views.py +++ /dev/null @@ -1,739 +0,0 @@ -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.db.models import Sum -from django.http import HttpRequest, JsonResponse -from django.shortcuts import render, get_object_or_404, redirect -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ - -from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \ - EditCompensationActionModalForm, RemoveCompensationActionModalForm -from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm -from compensation.forms.modals.state import NewCompensationStateModalForm, RemoveCompensationStateModalForm, \ - EditCompensationStateModalForm -from compensation.models import CompensationAction, CompensationState -from ema.forms import NewEmaForm, EditEmaForm, NewEmaDocumentModalForm -from ema.tables import EmaTable -from intervention.forms.modals.share import ShareModalForm -from konova.contexts import BaseContext -from konova.decorators import conservation_office_group_required, shared_access_required -from ema.models import Ema, EmaDocument -from konova.forms.modals import RemoveModalForm, RecordModalForm, RemoveDeadlineModalForm, \ - EditDocumentModalForm, ResubmissionModalForm -from konova.forms import SimpleGeomForm -from konova.models import Deadline -from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.documents import get_document, remove_document -from konova.utils.generators import generate_qr_code -from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \ - DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, \ - COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, \ - COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, RECORDED_BLOCKS_EDIT -from konova.utils.user_checks import in_group - - -@login_required -def index_view(request: HttpRequest): - """ Renders the index view for EMAs - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "generic_index.html" - emas = Ema.objects.filter( - deleted=None, - ).order_by( - "-modified" - ) - table = EmaTable( - request, - queryset=emas - ) - context = { - "table": table, - TAB_TITLE_IDENTIFIER: _("EMAs - Overview"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@conservation_office_group_required -def new_view(request: HttpRequest): - """ - Renders a view for a new eco account creation - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "ema/form/view.html" - data_form = NewEmaForm(request.POST or None) - geom_form = SimpleGeomForm(request.POST or None, read_only=False) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - generated_identifier = data_form.cleaned_data.get("identifier", None) - ema = data_form.save(request.user, geom_form) - if generated_identifier != ema.identifier: - messages.info( - request, - IDENTIFIER_REPLACED.format( - generated_identifier, - ema.identifier - ) - ) - messages.success(request, _("EMA {} added").format(ema.identifier)) - return redirect("ema:detail", id=ema.id) - else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("New EMA"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@conservation_office_group_required -def new_id_view(request: HttpRequest): - """ JSON endpoint - - Provides fetching of free identifiers for e.g. AJAX calls - - """ - tmp = Ema() - identifier = tmp.generate_new_identifier() - while Ema.objects.filter(identifier=identifier).exists(): - identifier = tmp.generate_new_identifier() - return JsonResponse( - data={ - "gen_data": identifier - } - ) - - -@login_required -def detail_view(request: HttpRequest, id: str): - """ Renders the detail view of an EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA id - - Returns: - - """ - template = "ema/detail/view.html" - ema = get_object_or_404(Ema, id=id, deleted=None) - - geom_form = SimpleGeomForm(instance=ema) - parcels = ema.get_underlying_parcels() - _user = request.user - is_data_shared = ema.is_shared_with(_user) - - # Order states according to surface - before_states = ema.before_states.all().order_by("-surface") - after_states = ema.after_states.all().order_by("-surface") - - # Precalculate logical errors between before- and after-states - # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling - sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0 - sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 - diff_states = abs(sum_before_states - sum_after_states) - - ema.set_status_messages(request) - - context = { - "obj": ema, - "geom_form": geom_form, - "parcels": parcels, - "has_access": is_data_shared, - "before_states": before_states, - "after_states": after_states, - "sum_before_states": sum_before_states, - "sum_after_states": sum_after_states, - "diff_states": diff_states, - "is_default_member": in_group(_user, DEFAULT_GROUP), - "is_zb_member": in_group(_user, ZB_GROUP), - "is_ets_member": in_group(_user, ETS_GROUP), - "LANIS_LINK": ema.get_LANIS_link(), - TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}", - "has_finished_deadlines": ema.get_finished_deadlines().exists(), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def log_view(request: HttpRequest, id: str): - """ Renders a log view using modal - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - template = "modal/modal_generic.html" - body_template = "log.html" - - context = { - "modal_body_template": body_template, - "log": ema.log.all(), - "modal_title": _("Log"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing compensations - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "compensation/form/view.html" - # Get object from db - ema = get_object_or_404(Ema, id=id) - if ema.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("ema:detail", id=id) - - # Create forms, initialize with values from db/from POST request - data_form = EditEmaForm(request.POST or None, instance=ema) - geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - # The data form takes the geom form for processing, as well as the performing user - ema = data_form.save(request.user, geom_form) - messages.success(request, _("EMA {} edited").format(ema.identifier)) - return redirect("ema:detail", id=ema.id) - else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def remove_view(request: HttpRequest, id: str): - """ Renders a modal view for removing the EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = RemoveModalForm(request.POST or None, instance=ema, request=request) - return form.process_request( - request=request, - msg_success=_("EMA removed"), - redirect_url=reverse("ema:index"), - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def record_view(request: HttpRequest, id: str): - """ Renders a modal view for recording the EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - msg_succ = _("{} unrecorded") if ema.recorded else _("{} recorded") - form = RecordModalForm(request.POST or None, instance=ema, request=request) - return form.process_request( - request=request, - msg_success=msg_succ.format("EMA"), - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def state_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for an EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id to which the new state will be related - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = NewCompensationStateModalForm(request.POST or None, instance=ema, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_ADDED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def action_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new actions for an EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id to which the new state will be related - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = NewCompensationActionModalForm(request.POST or None, instance=ema, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_ADDED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def action_edit_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for editing an actions for an EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id - action_id (str): The action id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = EditCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_EDITED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def deadline_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for an EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id to which the new state will be related - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = NewDeadlineModalForm(request.POST or None, instance=ema, request=request) - return form.process_request( - request, - msg_success=DEADLINE_ADDED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def document_new_view(request: HttpRequest, id: str): - """ Renders a form for uploading new documents - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id to which the new document will be related - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = NewEmaDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, request=request) - return form.process_request( - request, - msg_success=DOCUMENT_ADDED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def get_document_view(request: HttpRequest, id: str, doc_id: str): - """ Returns the document as downloadable file - - Wraps the generic document fetcher function from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The EMA id - doc_id (str): The document id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - doc = get_object_or_404(EmaDocument, id=doc_id) - return get_document(doc) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def edit_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The EMA id - doc_id (str): The document id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - doc = get_object_or_404(EmaDocument, id=doc_id) - form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, document=doc, request=request) - return form.process_request( - request, - DOCUMENT_EDITED, - reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def remove_document_view(request: HttpRequest, id:str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The EMA id - doc_id (str): The document id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - doc = get_object_or_404(EmaDocument, id=doc_id) - return remove_document( - request, - doc - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def state_remove_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for removing an EMA state - - Args: - request (HttpRequest): The incoming request - id (str): The ema id - state_id (str): The state's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = RemoveCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_REMOVED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def state_edit_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for editing an EMA state - - Args: - request (HttpRequest): The incoming request - id (str): The ema id - state_id (str): The state's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = EditCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_EDITED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def action_remove_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for removing an EMA action - - Args: - request (HttpRequest): The incoming request - id (str): The ema id - id (str): The action's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = RemoveCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_REMOVED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -def report_view(request:HttpRequest, id: str): - """ Renders the public report view - - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention - - Returns: - - """ - # Reuse the compensation report template since EMAs are structurally identical - template = "ema/report/report.html" - ema = get_object_or_404(Ema, id=id) - - tab_title = _("Report {}").format(ema.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not ema.is_ready_for_publish(): - template = "report/unavailable.html" - context = { - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) - - # Prepare data for map viewer - geom_form = SimpleGeomForm( - instance=ema, - ) - parcels = ema.get_underlying_parcels() - - qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,))) - qrcode_img = generate_qr_code(qrcode_url, 10) - qrcode_lanis_url = ema.get_LANIS_link() - qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) - - # Order states by surface - before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type") - after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type") - actions = ema.actions.all().prefetch_related("action_type") - - context = { - "obj": ema, - "qrcode": { - "img": qrcode_img, - "url": qrcode_url - }, - "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url - }, - "has_access": False, # disables action buttons during rendering - "before_states": before_states, - "after_states": after_states, - "geom_form": geom_form, - "parcels": parcels, - "actions": actions, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -def share_view(request: HttpRequest, id: str, token: str): - """ Performs sharing of an ema - - If token given in url is not valid, the user will be redirected to the dashboard - - Args: - request (HttpRequest): The incoming request - id (str): EMA's id - token (str): Access token for EMA - - Returns: - - """ - user = request.user - obj = get_object_or_404(Ema, id=id) - # Check tokens - if obj.access_token == token: - # Send different messages in case user has already been added to list of sharing users - if obj.is_shared_with(user): - messages.info( - request, - _("{} has already been shared with you").format(obj.identifier) - ) - else: - messages.success( - request, - _("{} has been shared with you").format(obj.identifier) - ) - obj.share_with_user(user) - return redirect("ema:detail", id=id) - else: - messages.error( - request, - _("Share link invalid"), - extra_tags="danger", - ) - return redirect("home") - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def create_share_view(request: HttpRequest, id: str): - """ Renders sharing form for an Ema - - Args: - request (HttpRequest): The incoming request - id (str): Ema's id - - Returns: - - """ - obj = get_object_or_404(Ema, id=id) - form = ShareModalForm(request.POST or None, instance=obj, request=request) - return form.process_request( - request, - msg_success=_("Share settings updated") - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for editing deadlines from a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = EditDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_EDITED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for removing deadlines from a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = RemoveDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_REMOVED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def create_resubmission_view(request: HttpRequest, id: str): - """ Renders resubmission form for an EMA - - Args: - request (HttpRequest): The incoming request - id (str): EMA's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = ResubmissionModalForm(request.POST or None, instance=ema, request=request) - form.action_url = reverse("ema:resubmission-create", args=(id,)) - return form.process_request( - request, - msg_success=_("Resubmission set"), - redirect_url=reverse("ema:detail", args=(id,)) - ) diff --git a/ema/views/__init__.py b/ema/views/__init__.py new file mode 100644 index 00000000..3a996e1f --- /dev/null +++ b/ema/views/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" diff --git a/ema/views/action.py b/ema/views/action.py new file mode 100644 index 00000000..bd13646c --- /dev/null +++ b/ema/views/action.py @@ -0,0 +1,89 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.compensation_action import RemoveCompensationActionModalForm, \ + EditCompensationActionModalForm, NewCompensationActionModalForm +from compensation.models import CompensationAction +from ema.models import Ema +from konova.decorators import shared_access_required, conservation_office_group_required +from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_EDITED, \ + COMPENSATION_ACTION_ADDED + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def action_new_view(request: HttpRequest, id: str): + """ Renders a form for adding new actions for an EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id to which the new state will be related + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + form = NewCompensationActionModalForm(request.POST or None, instance=ema, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_ACTION_ADDED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def action_edit_view(request: HttpRequest, id: str, action_id: str): + """ Renders a form for editing an actions for an EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id + action_id (str): The action id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + action = get_object_or_404(CompensationAction, id=action_id) + form = EditCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_ACTION_EDITED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def action_remove_view(request: HttpRequest, id: str, action_id: str): + """ Renders a form for removing an EMA action + + Args: + request (HttpRequest): The incoming request + id (str): The ema id + id (str): The action's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + action = get_object_or_404(CompensationAction, id=action_id) + form = RemoveCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_ACTION_REMOVED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) diff --git a/ema/views/deadline.py b/ema/views/deadline.py new file mode 100644 index 00000000..223ceeb0 --- /dev/null +++ b/ema/views/deadline.py @@ -0,0 +1,90 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.deadline import EditDeadlineModalForm, NewDeadlineModalForm +from ema.models import Ema +from konova.decorators import shared_access_required, conservation_office_group_required +from konova.forms.modals import RemoveDeadlineModalForm +from konova.models import Deadline +from konova.utils.message_templates import DEADLINE_REMOVED, DEADLINE_EDITED, DEADLINE_ADDED + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def deadline_new_view(request: HttpRequest, id: str): + """ Renders a form for adding new states for an EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id to which the new state will be related + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + form = NewDeadlineModalForm(request.POST or None, instance=ema, request=request) + return form.process_request( + request, + msg_success=DEADLINE_ADDED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str): + """ Renders a form for editing deadlines from a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + deadline_id (str): The deadline's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + deadline = get_object_or_404(Deadline, id=deadline_id) + form = EditDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request) + return form.process_request( + request, + msg_success=DEADLINE_EDITED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): + """ Renders a form for removing deadlines from a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + deadline_id (str): The deadline's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + deadline = get_object_or_404(Deadline, id=deadline_id) + form = RemoveDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request) + return form.process_request( + request, + msg_success=DEADLINE_REMOVED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + diff --git a/ema/views/document.py b/ema/views/document.py new file mode 100644 index 00000000..faacb5bc --- /dev/null +++ b/ema/views/document.py @@ -0,0 +1,111 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from ema.forms import NewEmaDocumentModalForm +from ema.models import Ema, EmaDocument +from konova.decorators import shared_access_required, conservation_office_group_required +from konova.forms.modals import EditDocumentModalForm +from konova.utils.documents import get_document, remove_document +from konova.utils.message_templates import DOCUMENT_ADDED, DOCUMENT_EDITED + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def document_new_view(request: HttpRequest, id: str): + """ Renders a form for uploading new documents + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id to which the new document will be related + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + form = NewEmaDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, request=request) + return form.process_request( + request, + msg_success=DOCUMENT_ADDED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def get_document_view(request: HttpRequest, id: str, doc_id: str): + """ Returns the document as downloadable file + + Wraps the generic document fetcher function from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The EMA id + doc_id (str): The document id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + doc = get_object_or_404(EmaDocument, id=doc_id) + return get_document(doc) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def edit_document_view(request: HttpRequest, id: str, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The EMA id + doc_id (str): The document id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + doc = get_object_or_404(EmaDocument, id=doc_id) + form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, document=doc, request=request) + return form.process_request( + request, + DOCUMENT_EDITED, + reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def remove_document_view(request: HttpRequest, id:str, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The EMA id + doc_id (str): The document id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + doc = get_object_or_404(EmaDocument, id=doc_id) + return remove_document( + request, + doc + ) + diff --git a/ema/views/ema.py b/ema/views/ema.py new file mode 100644 index 00000000..f2b4d3b8 --- /dev/null +++ b/ema/views/ema.py @@ -0,0 +1,237 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.db.models import Sum +from django.http import HttpRequest, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from ema.forms import NewEmaForm, EditEmaForm +from ema.models import Ema +from ema.tables import EmaTable +from konova.contexts import BaseContext +from konova.decorators import shared_access_required, conservation_office_group_required +from konova.forms import SimpleGeomForm +from konova.forms.modals import RemoveModalForm +from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID +from konova.utils.user_checks import in_group + + +@login_required +def index_view(request: HttpRequest): + """ Renders the index view for EMAs + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + template = "generic_index.html" + emas = Ema.objects.filter( + deleted=None, + ).order_by( + "-modified" + ) + table = EmaTable( + request, + queryset=emas + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: _("EMAs - Overview"), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@conservation_office_group_required +def new_view(request: HttpRequest): + """ + Renders a view for a new eco account creation + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + template = "ema/form/view.html" + data_form = NewEmaForm(request.POST or None) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) + if request.method == "POST": + if data_form.is_valid() and geom_form.is_valid(): + generated_identifier = data_form.cleaned_data.get("identifier", None) + ema = data_form.save(request.user, geom_form) + if generated_identifier != ema.identifier: + messages.info( + request, + IDENTIFIER_REPLACED.format( + generated_identifier, + ema.identifier + ) + ) + messages.success(request, _("EMA {} added").format(ema.identifier)) + return redirect("ema:detail", id=ema.id) + else: + messages.error(request, FORM_INVALID, extra_tags="danger",) + else: + # For clarification: nothing in this case + pass + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New EMA"), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@conservation_office_group_required +def new_id_view(request: HttpRequest): + """ JSON endpoint + + Provides fetching of free identifiers for e.g. AJAX calls + + """ + tmp = Ema() + identifier = tmp.generate_new_identifier() + while Ema.objects.filter(identifier=identifier).exists(): + identifier = tmp.generate_new_identifier() + return JsonResponse( + data={ + "gen_data": identifier + } + ) + + +@login_required +def detail_view(request: HttpRequest, id: str): + """ Renders the detail view of an EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA id + + Returns: + + """ + template = "ema/detail/view.html" + ema = get_object_or_404(Ema, id=id, deleted=None) + + geom_form = SimpleGeomForm(instance=ema) + parcels = ema.get_underlying_parcels() + _user = request.user + is_data_shared = ema.is_shared_with(_user) + + # Order states according to surface + before_states = ema.before_states.all().order_by("-surface") + after_states = ema.after_states.all().order_by("-surface") + + # Precalculate logical errors between before- and after-states + # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling + sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0 + sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 + diff_states = abs(sum_before_states - sum_after_states) + + ema.set_status_messages(request) + + context = { + "obj": ema, + "geom_form": geom_form, + "parcels": parcels, + "has_access": is_data_shared, + "before_states": before_states, + "after_states": after_states, + "sum_before_states": sum_before_states, + "sum_after_states": sum_after_states, + "diff_states": diff_states, + "is_default_member": in_group(_user, DEFAULT_GROUP), + "is_zb_member": in_group(_user, ZB_GROUP), + "is_ets_member": in_group(_user, ETS_GROUP), + "LANIS_LINK": ema.get_LANIS_link(), + TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}", + "has_finished_deadlines": ema.get_finished_deadlines().exists(), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def edit_view(request: HttpRequest, id: str): + """ + Renders a view for editing compensations + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + template = "compensation/form/view.html" + # Get object from db + ema = get_object_or_404(Ema, id=id) + if ema.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("ema:detail", id=id) + + # Create forms, initialize with values from db/from POST request + data_form = EditEmaForm(request.POST or None, instance=ema) + geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema) + if request.method == "POST": + if data_form.is_valid() and geom_form.is_valid(): + # The data form takes the geom form for processing, as well as the performing user + ema = data_form.save(request.user, geom_form) + messages.success(request, _("EMA {} edited").format(ema.identifier)) + return redirect("ema:detail", id=ema.id) + else: + messages.error(request, FORM_INVALID, extra_tags="danger",) + else: + # For clarification: nothing in this case + pass + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def remove_view(request: HttpRequest, id: str): + """ Renders a modal view for removing the EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + form = RemoveModalForm(request.POST or None, instance=ema, request=request) + return form.process_request( + request=request, + msg_success=_("EMA removed"), + redirect_url=reverse("ema:index"), + ) + diff --git a/ema/views/log.py b/ema/views/log.py new file mode 100644 index 00000000..3e3f869f --- /dev/null +++ b/ema/views/log.py @@ -0,0 +1,41 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404, render +from django.utils.translation import gettext_lazy as _ + +from ema.models import Ema +from konova.contexts import BaseContext +from konova.decorators import shared_access_required, conservation_office_group_required + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def log_view(request: HttpRequest, id: str): + """ Renders a log view using modal + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + template = "modal/modal_generic.html" + body_template = "log.html" + + context = { + "modal_body_template": body_template, + "log": ema.log.all(), + "modal_title": _("Log"), + } + context = BaseContext(request, context).context + return render(request, template, context) diff --git a/ema/views/record.py b/ema/views/record.py new file mode 100644 index 00000000..4ae40d2f --- /dev/null +++ b/ema/views/record.py @@ -0,0 +1,37 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext_lazy as _ + +from ema.models import Ema +from konova.decorators import shared_access_required, conservation_office_group_required +from konova.forms.modals import RecordModalForm + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def record_view(request: HttpRequest, id: str): + """ Renders a modal view for recording the EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + msg_succ = _("{} unrecorded") if ema.recorded else _("{} recorded") + form = RecordModalForm(request.POST or None, instance=ema, request=request) + return form.process_request( + request=request, + msg_success=msg_succ.format("EMA"), + ) diff --git a/ema/views/report.py b/ema/views/report.py new file mode 100644 index 00000000..daea0b44 --- /dev/null +++ b/ema/views/report.py @@ -0,0 +1,79 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.http import HttpRequest +from django.shortcuts import get_object_or_404, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from ema.models import Ema +from konova.contexts import BaseContext +from konova.forms import SimpleGeomForm +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.utils.generators import generate_qr_code + + +def report_view(request:HttpRequest, id: str): + """ Renders the public report view + + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention + + Returns: + + """ + # Reuse the compensation report template since EMAs are structurally identical + template = "ema/report/report.html" + ema = get_object_or_404(Ema, id=id) + + tab_title = _("Report {}").format(ema.identifier) + # If intervention is not recorded (yet or currently) we need to render another template without any data + if not ema.is_ready_for_publish(): + template = "report/unavailable.html" + context = { + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) + + # Prepare data for map viewer + geom_form = SimpleGeomForm( + instance=ema, + ) + parcels = ema.get_underlying_parcels() + + qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,))) + qrcode_img = generate_qr_code(qrcode_url, 10) + qrcode_lanis_url = ema.get_LANIS_link() + qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) + + # Order states by surface + before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type") + after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type") + actions = ema.actions.all().prefetch_related("action_type") + + context = { + "obj": ema, + "qrcode": { + "img": qrcode_img, + "url": qrcode_url + }, + "qrcode_lanis": { + "img": qrcode_img_lanis, + "url": qrcode_lanis_url + }, + "has_access": False, # disables action buttons during rendering + "before_states": before_states, + "after_states": after_states, + "geom_form": geom_form, + "parcels": parcels, + "actions": actions, + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) diff --git a/ema/views/resubmission.py b/ema/views/resubmission.py new file mode 100644 index 00000000..838cab3a --- /dev/null +++ b/ema/views/resubmission.py @@ -0,0 +1,39 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from ema.models import Ema +from konova.decorators import shared_access_required, conservation_office_group_required +from konova.forms.modals import ResubmissionModalForm + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def create_resubmission_view(request: HttpRequest, id: str): + """ Renders resubmission form for an EMA + + Args: + request (HttpRequest): The incoming request + id (str): EMA's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + form = ResubmissionModalForm(request.POST or None, instance=ema, request=request) + form.action_url = reverse("ema:resubmission-create", args=(id,)) + return form.process_request( + request, + msg_success=_("Resubmission set"), + redirect_url=reverse("ema:detail", args=(id,)) + ) diff --git a/ema/views/share.py b/ema/views/share.py new file mode 100644 index 00000000..83aae91d --- /dev/null +++ b/ema/views/share.py @@ -0,0 +1,77 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404, redirect +from django.utils.translation import gettext_lazy as _ + +from ema.models import Ema +from intervention.forms.modals.share import ShareModalForm +from konova.decorators import conservation_office_group_required, shared_access_required + + +@login_required +def share_view(request: HttpRequest, id: str, token: str): + """ Performs sharing of an ema + + If token given in url is not valid, the user will be redirected to the dashboard + + Args: + request (HttpRequest): The incoming request + id (str): EMA's id + token (str): Access token for EMA + + Returns: + + """ + user = request.user + obj = get_object_or_404(Ema, id=id) + # Check tokens + if obj.access_token == token: + # Send different messages in case user has already been added to list of sharing users + if obj.is_shared_with(user): + messages.info( + request, + _("{} has already been shared with you").format(obj.identifier) + ) + else: + messages.success( + request, + _("{} has been shared with you").format(obj.identifier) + ) + obj.share_with_user(user) + return redirect("ema:detail", id=id) + else: + messages.error( + request, + _("Share link invalid"), + extra_tags="danger", + ) + return redirect("home") + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def create_share_view(request: HttpRequest, id: str): + """ Renders sharing form for an Ema + + Args: + request (HttpRequest): The incoming request + id (str): Ema's id + + Returns: + + """ + obj = get_object_or_404(Ema, id=id) + form = ShareModalForm(request.POST or None, instance=obj, request=request) + return form.process_request( + request, + msg_success=_("Share settings updated") + ) diff --git a/ema/views/state.py b/ema/views/state.py new file mode 100644 index 00000000..1c7b0a44 --- /dev/null +++ b/ema/views/state.py @@ -0,0 +1,90 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.state import NewCompensationStateModalForm, RemoveCompensationStateModalForm, \ + EditCompensationStateModalForm +from compensation.models import CompensationState +from ema.models import Ema +from konova.decorators import conservation_office_group_required, shared_access_required +from konova.utils.message_templates import COMPENSATION_STATE_ADDED, COMPENSATION_STATE_REMOVED, \ + COMPENSATION_STATE_EDITED + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def state_new_view(request: HttpRequest, id: str): + """ Renders a form for adding new states for an EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id to which the new state will be related + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + form = NewCompensationStateModalForm(request.POST or None, instance=ema, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_ADDED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def state_remove_view(request: HttpRequest, id: str, state_id: str): + """ Renders a form for removing an EMA state + + Args: + request (HttpRequest): The incoming request + id (str): The ema id + state_id (str): The state's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + state = get_object_or_404(CompensationState, id=state_id) + form = RemoveCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_REMOVED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def state_edit_view(request: HttpRequest, id: str, state_id: str): + """ Renders a form for editing an EMA state + + Args: + request (HttpRequest): The incoming request + id (str): The ema id + state_id (str): The state's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + state = get_object_or_404(CompensationState, id=state_id) + form = EditCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_EDITED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + From 8c3e8b596ac9c417b266314f7761e607b31214d3 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 19 Aug 2022 08:12:32 +0200 Subject: [PATCH 20/48] Compensation views * splits compensation/views/compensation.py (+700 lines) into separate files in new module * view files can now be found in /compensation/views/compensation/... --- compensation/urls/compensation.py | 12 +- compensation/views/__init__.py | 1 - compensation/views/compensation.py | 685 ------------------ compensation/views/compensation/__init__.py | 7 + compensation/views/compensation/action.py | 89 +++ .../views/compensation/compensation.py | 272 +++++++ compensation/views/compensation/deadline.py | 89 +++ compensation/views/compensation/document.py | 111 +++ compensation/views/compensation/log.py | 41 ++ compensation/views/compensation/report.py | 79 ++ .../views/compensation/resubmission.py | 39 + compensation/views/compensation/state.py | 89 +++ 12 files changed, 827 insertions(+), 687 deletions(-) delete mode 100644 compensation/views/compensation.py create mode 100644 compensation/views/compensation/__init__.py create mode 100644 compensation/views/compensation/action.py create mode 100644 compensation/views/compensation/compensation.py create mode 100644 compensation/views/compensation/deadline.py create mode 100644 compensation/views/compensation/document.py create mode 100644 compensation/views/compensation/log.py create mode 100644 compensation/views/compensation/report.py create mode 100644 compensation/views/compensation/resubmission.py create mode 100644 compensation/views/compensation/state.py diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index 66020055..75ec9297 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -6,7 +6,17 @@ Created on: 24.08.21 """ from django.urls import path -from compensation.views.compensation import * + +from compensation.views.compensation.document import edit_document_view, new_document_view, remove_document_view, \ + get_document_view +from compensation.views.compensation.resubmission import create_resubmission_view +from compensation.views.compensation.report import report_view +from compensation.views.compensation.deadline import deadline_new_view, deadline_edit_view, deadline_remove_view +from compensation.views.compensation.action import action_edit_view, action_new_view, action_remove_view +from compensation.views.compensation.state import state_new_view, state_remove_view, state_edit_view +from compensation.views.compensation.compensation import index_view, new_view, new_id_view, detail_view, edit_view, \ + remove_view +from compensation.views.compensation.log import log_view urlpatterns = [ # Main compensation diff --git a/compensation/views/__init__.py b/compensation/views/__init__.py index db03b5a1..dc6f522a 100644 --- a/compensation/views/__init__.py +++ b/compensation/views/__init__.py @@ -5,6 +5,5 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 16.11.21 """ -from .compensation import * from .eco_account import * from .payment import * diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py deleted file mode 100644 index c1bec047..00000000 --- a/compensation/views/compensation.py +++ /dev/null @@ -1,685 +0,0 @@ -from django.contrib.auth.decorators import login_required -from django.core.exceptions import ObjectDoesNotExist -from django.db.models import Sum -from django.http import HttpRequest, JsonResponse -from django.shortcuts import render -from django.utils.translation import gettext_lazy as _ - -from compensation.forms.compensation import NewCompensationForm, EditCompensationForm -from compensation.forms.modals.state import NewCompensationStateModalForm, RemoveCompensationStateModalForm, EditCompensationStateModalForm -from compensation.forms.modals.document import NewCompensationDocumentModalForm -from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \ - EditCompensationActionModalForm, RemoveCompensationActionModalForm -from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm -from compensation.models import Compensation, CompensationState, CompensationAction, CompensationDocument -from compensation.tables.compensation import CompensationTable -from intervention.models import Intervention -from konova.contexts import BaseContext -from konova.decorators import * -from konova.forms.modals import RemoveModalForm,RemoveDeadlineModalForm, EditDocumentModalForm, \ - ResubmissionModalForm -from konova.forms import SimpleGeomForm -from konova.models import Deadline -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.documents import get_document, remove_document -from konova.utils.generators import generate_qr_code -from konova.utils.message_templates import FORM_INVALID, IDENTIFIER_REPLACED, \ - CHECKED_RECORDED_RESET, COMPENSATION_ADDED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, DOCUMENT_ADDED, \ - COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, \ - DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, \ - DEADLINE_EDITED, RECORDED_BLOCKS_EDIT, PARAMS_INVALID, DATA_CHECKED_PREVIOUSLY_TEMPLATE -from konova.utils.user_checks import in_group - - -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ - Renders the index view for compensation - - Args: - request (HttpRequest): The incoming request - - Returns: - A rendered view - """ - template = "generic_index.html" - compensations = Compensation.objects.filter( - deleted=None, # only show those which are not deleted individually - intervention__deleted=None, # and don't show the ones whose intervention has been deleted - ) - table = CompensationTable( - request=request, - queryset=compensations - ) - context = { - "table": table, - TAB_TITLE_IDENTIFIER: _("Compensations - Overview"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "intervention_id") -def new_view(request: HttpRequest, intervention_id: str = None): - """ - Renders a view for a new compensation creation - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "compensation/form/view.html" - if intervention_id is not None: - try: - intervention = Intervention.objects.get(id=intervention_id) - except ObjectDoesNotExist: - messages.error(request, PARAMS_INVALID) - return redirect("home") - if intervention.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("intervention:detail", id=intervention_id) - - data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id) - geom_form = SimpleGeomForm(request.POST or None, read_only=False) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - generated_identifier = data_form.cleaned_data.get("identifier", None) - comp = data_form.save(request.user, geom_form) - if generated_identifier != comp.identifier: - messages.info( - request, - IDENTIFIER_REPLACED.format( - generated_identifier, - comp.identifier - ) - ) - messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier)) - return redirect("compensation:detail", id=comp.id) - else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("New compensation"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -def new_id_view(request: HttpRequest): - """ JSON endpoint - - Provides fetching of free identifiers for e.g. AJAX calls - - """ - tmp = Compensation() - identifier = tmp.generate_new_identifier() - while Compensation.objects.filter(identifier=identifier).exists(): - identifier = tmp.generate_new_identifier() - return JsonResponse( - data={ - "gen_data": identifier - } - ) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing compensations - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "compensation/form/view.html" - # Get object from db - comp = get_object_or_404(Compensation, id=id) - if comp.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("compensation:detail", id=id) - - # Create forms, initialize with values from db/from POST request - data_form = EditCompensationForm(request.POST or None, instance=comp) - geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - # Preserve state of intervention recorded/checked to determine whether the user must be informed or not - # about a change of the recorded/checked state - intervention_recorded = comp.intervention.recorded is not None - intervention_checked = comp.intervention.checked is not None - - # The data form takes the geom form for processing, as well as the performing user - comp = data_form.save(request.user, geom_form) - if intervention_recorded or intervention_checked: - messages.info(request, CHECKED_RECORDED_RESET) - messages.success(request, _("Compensation {} edited").format(comp.identifier)) - return redirect("compensation:detail", id=comp.id) - else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@any_group_check -def detail_view(request: HttpRequest, id: str): - """ Renders a detail view for a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - - Returns: - - """ - template = "compensation/detail/compensation/view.html" - comp = get_object_or_404(Compensation, id=id) - geom_form = SimpleGeomForm(instance=comp) - parcels = comp.get_underlying_parcels() - _user = request.user - is_data_shared = comp.intervention.is_shared_with(_user) - - # Order states according to surface - before_states = comp.before_states.all().prefetch_related("biotope_type").order_by("-surface") - after_states = comp.after_states.all().prefetch_related("biotope_type").order_by("-surface") - actions = comp.actions.all().prefetch_related("action_type") - - # Precalculate logical errors between before- and after-states - # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling - sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0 - sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 - diff_states = abs(sum_before_states - sum_after_states) - - request = comp.set_status_messages(request) - - last_checked = comp.intervention.get_last_checked_action() - last_checked_tooltip = "" - if last_checked: - last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user) - - context = { - "obj": comp, - "last_checked": last_checked, - "last_checked_tooltip": last_checked_tooltip, - "geom_form": geom_form, - "parcels": parcels, - "has_access": is_data_shared, - "actions": actions, - "before_states": before_states, - "after_states": after_states, - "sum_before_states": sum_before_states, - "sum_after_states": sum_after_states, - "diff_states": diff_states, - "is_default_member": in_group(_user, DEFAULT_GROUP), - "is_zb_member": in_group(_user, ZB_GROUP), - "is_ets_member": in_group(_user, ETS_GROUP), - "LANIS_LINK": comp.get_LANIS_link(), - TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}", - "has_finished_deadlines": comp.get_finished_deadlines().exists(), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def log_view(request: HttpRequest, id: str): - """ Renders a log view using modal - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - template = "modal/modal_generic.html" - body_template = "log.html" - - context = { - "modal_body_template": body_template, - "log": comp.log.all(), - "modal_title": _("Log"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def remove_view(request: HttpRequest, id: str): - """ Renders a modal view for removing the compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - form = RemoveModalForm(request.POST or None, instance=comp, request=request) - return form.process_request( - request=request, - msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier), - redirect_url=reverse("compensation:index"), - ) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def new_document_view(request: HttpRequest, id: str): - """ Renders a form for uploading new documents - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id to which the new document will be related - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - form = NewCompensationDocumentModalForm(request.POST or None, request.FILES or None, instance=comp, request=request) - return form.process_request( - request, - msg_success=DOCUMENT_ADDED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def get_document_view(request: HttpRequest, id: str, doc_id: str): - """ Returns the document as downloadable file - - Wraps the generic document fetcher function from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The compensation id - doc_id (str): The document id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - doc = get_object_or_404(CompensationDocument, id=doc_id) - return get_document(doc) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def remove_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The compensation id - doc_id (str): The document id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - doc = get_object_or_404(CompensationDocument, id=doc_id) - return remove_document( - request, - doc - ) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def edit_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The compensation id - doc_id (str): The document id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - doc = get_object_or_404(CompensationDocument, id=doc_id) - form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=comp, document=doc, request=request) - return form.process_request( - request, - DOCUMENT_EDITED, - reverse("compensation:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def state_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id to which the new state will be related - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - form = NewCompensationStateModalForm(request.POST or None, instance=comp, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_ADDED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def action_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new actions for a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id to which the new state will be related - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - form = NewCompensationActionModalForm(request.POST or None, instance=comp, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_ADDED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def action_edit_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for editing actions for a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - action_id (str): The action's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = EditCompensationActionModalForm(request.POST or None, instance=comp, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_EDITED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def deadline_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id to which the new state will be related - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - form = NewDeadlineModalForm(request.POST or None, instance=comp, request=request) - return form.process_request( - request, - msg_success=DEADLINE_ADDED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for editing deadlines from a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = EditDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_EDITED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for removing deadlines from a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = RemoveDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_REMOVED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def state_remove_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for removing a compensation state - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - state_id (str): The state's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = RemoveCompensationStateModalForm(request.POST or None, instance=comp, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_REMOVED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def state_edit_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for editing a compensation state - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - state_id (str): The state's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = EditCompensationStateModalForm(request.POST or None, instance=comp, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_EDITED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def action_remove_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for removing a compensation action - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - id (str): The action's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = RemoveCompensationActionModalForm(request.POST or None, instance=comp, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_REMOVED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) - - -def report_view(request: HttpRequest, id: str): - """ Renders the public report view - - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention - - Returns: - - """ - # Reuse the compensation report template since compensations are structurally identical - template = "compensation/report/compensation/report.html" - comp = get_object_or_404(Compensation, id=id) - - tab_title = _("Report {}").format(comp.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not comp.is_ready_for_publish(): - template = "report/unavailable.html" - context = { - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) - - # Prepare data for map viewer - geom_form = SimpleGeomForm( - instance=comp - ) - parcels = comp.get_underlying_parcels() - - qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,))) - qrcode_img = generate_qr_code(qrcode_url, 10) - qrcode_lanis_url = comp.get_LANIS_link() - qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) - - # Order states by surface - before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type") - after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type") - actions = comp.actions.all().prefetch_related("action_type") - - context = { - "obj": comp, - "qrcode": { - "img": qrcode_img, - "url": qrcode_url, - }, - "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url, - }, - "has_access": False, # disables action buttons during rendering - "before_states": before_states, - "after_states": after_states, - "geom_form": geom_form, - "parcels": parcels, - "actions": actions, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def create_resubmission_view(request: HttpRequest, id: str): - """ Renders resubmission form for a compensation - - Args: - request (HttpRequest): The incoming request - id (str): Compensation's id - - Returns: - - """ - com = get_object_or_404(Compensation, id=id) - form = ResubmissionModalForm(request.POST or None, instance=com, request=request) - form.action_url = reverse("compensation:resubmission-create", args=(id,)) - return form.process_request( - request, - msg_success=_("Resubmission set"), - redirect_url=reverse("compensation:detail", args=(id,)) - ) diff --git a/compensation/views/compensation/__init__.py b/compensation/views/compensation/__init__.py new file mode 100644 index 00000000..3a996e1f --- /dev/null +++ b/compensation/views/compensation/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" diff --git a/compensation/views/compensation/action.py b/compensation/views/compensation/action.py new file mode 100644 index 00000000..54f6bc39 --- /dev/null +++ b/compensation/views/compensation/action.py @@ -0,0 +1,89 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.compensation_action import RemoveCompensationActionModalForm, \ + EditCompensationActionModalForm, NewCompensationActionModalForm +from compensation.models import Compensation, CompensationAction +from konova.decorators import shared_access_required, default_group_required +from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_EDITED, \ + COMPENSATION_ACTION_ADDED + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def action_new_view(request: HttpRequest, id: str): + """ Renders a form for adding new actions for a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id to which the new state will be related + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + form = NewCompensationActionModalForm(request.POST or None, instance=comp, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_ACTION_ADDED, + redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def action_edit_view(request: HttpRequest, id: str, action_id: str): + """ Renders a form for editing actions for a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + action_id (str): The action's id + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + action = get_object_or_404(CompensationAction, id=action_id) + form = EditCompensationActionModalForm(request.POST or None, instance=comp, action=action, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_ACTION_EDITED, + redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def action_remove_view(request: HttpRequest, id: str, action_id: str): + """ Renders a form for removing a compensation action + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + id (str): The action's id + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + action = get_object_or_404(CompensationAction, id=action_id) + form = RemoveCompensationActionModalForm(request.POST or None, instance=comp, action=action, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_ACTION_REMOVED, + redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" + ) + diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py new file mode 100644 index 00000000..dd461100 --- /dev/null +++ b/compensation/views/compensation/compensation.py @@ -0,0 +1,272 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.core.exceptions import ObjectDoesNotExist +from django.db.models import Sum +from django.http import HttpRequest, JsonResponse +from django.shortcuts import get_object_or_404, render, redirect +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from compensation.forms.compensation import EditCompensationForm, NewCompensationForm +from compensation.models import Compensation +from compensation.tables.compensation import CompensationTable +from intervention.models import Intervention +from konova.contexts import BaseContext +from konova.decorators import shared_access_required, default_group_required, any_group_check +from konova.forms import SimpleGeomForm +from konova.forms.modals import RemoveModalForm +from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ + RECORDED_BLOCKS_EDIT, CHECKED_RECORDED_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \ + COMPENSATION_ADDED_TEMPLATE +from konova.utils.user_checks import in_group + + +@login_required +@any_group_check +def index_view(request: HttpRequest): + """ + Renders the index view for compensation + + Args: + request (HttpRequest): The incoming request + + Returns: + A rendered view + """ + template = "generic_index.html" + compensations = Compensation.objects.filter( + deleted=None, # only show those which are not deleted individually + intervention__deleted=None, # and don't show the ones whose intervention has been deleted + ) + table = CompensationTable( + request=request, + queryset=compensations + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: _("Compensations - Overview"), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@default_group_required +@shared_access_required(Intervention, "intervention_id") +def new_view(request: HttpRequest, intervention_id: str = None): + """ + Renders a view for a new compensation creation + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + template = "compensation/form/view.html" + if intervention_id is not None: + try: + intervention = Intervention.objects.get(id=intervention_id) + except ObjectDoesNotExist: + messages.error(request, PARAMS_INVALID) + return redirect("home") + if intervention.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("intervention:detail", id=intervention_id) + + data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) + if request.method == "POST": + if data_form.is_valid() and geom_form.is_valid(): + generated_identifier = data_form.cleaned_data.get("identifier", None) + comp = data_form.save(request.user, geom_form) + if generated_identifier != comp.identifier: + messages.info( + request, + IDENTIFIER_REPLACED.format( + generated_identifier, + comp.identifier + ) + ) + messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier)) + return redirect("compensation:detail", id=comp.id) + else: + messages.error(request, FORM_INVALID, extra_tags="danger",) + else: + # For clarification: nothing in this case + pass + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New compensation"), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@default_group_required +def new_id_view(request: HttpRequest): + """ JSON endpoint + + Provides fetching of free identifiers for e.g. AJAX calls + + """ + tmp = Compensation() + identifier = tmp.generate_new_identifier() + while Compensation.objects.filter(identifier=identifier).exists(): + identifier = tmp.generate_new_identifier() + return JsonResponse( + data={ + "gen_data": identifier + } + ) + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def edit_view(request: HttpRequest, id: str): + """ + Renders a view for editing compensations + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + template = "compensation/form/view.html" + # Get object from db + comp = get_object_or_404(Compensation, id=id) + if comp.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("compensation:detail", id=id) + + # Create forms, initialize with values from db/from POST request + data_form = EditCompensationForm(request.POST or None, instance=comp) + geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp) + if request.method == "POST": + if data_form.is_valid() and geom_form.is_valid(): + # Preserve state of intervention recorded/checked to determine whether the user must be informed or not + # about a change of the recorded/checked state + intervention_recorded = comp.intervention.recorded is not None + intervention_checked = comp.intervention.checked is not None + + # The data form takes the geom form for processing, as well as the performing user + comp = data_form.save(request.user, geom_form) + if intervention_recorded or intervention_checked: + messages.info(request, CHECKED_RECORDED_RESET) + messages.success(request, _("Compensation {} edited").format(comp.identifier)) + return redirect("compensation:detail", id=comp.id) + else: + messages.error(request, FORM_INVALID, extra_tags="danger",) + else: + # For clarification: nothing in this case + pass + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@any_group_check +def detail_view(request: HttpRequest, id: str): + """ Renders a detail view for a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + + Returns: + + """ + template = "compensation/detail/compensation/view.html" + comp = get_object_or_404(Compensation, id=id) + geom_form = SimpleGeomForm(instance=comp) + parcels = comp.get_underlying_parcels() + _user = request.user + is_data_shared = comp.intervention.is_shared_with(_user) + + # Order states according to surface + before_states = comp.before_states.all().prefetch_related("biotope_type").order_by("-surface") + after_states = comp.after_states.all().prefetch_related("biotope_type").order_by("-surface") + actions = comp.actions.all().prefetch_related("action_type") + + # Precalculate logical errors between before- and after-states + # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling + sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0 + sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 + diff_states = abs(sum_before_states - sum_after_states) + + request = comp.set_status_messages(request) + + last_checked = comp.intervention.get_last_checked_action() + last_checked_tooltip = "" + if last_checked: + last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user) + + context = { + "obj": comp, + "last_checked": last_checked, + "last_checked_tooltip": last_checked_tooltip, + "geom_form": geom_form, + "parcels": parcels, + "has_access": is_data_shared, + "actions": actions, + "before_states": before_states, + "after_states": after_states, + "sum_before_states": sum_before_states, + "sum_after_states": sum_after_states, + "diff_states": diff_states, + "is_default_member": in_group(_user, DEFAULT_GROUP), + "is_zb_member": in_group(_user, ZB_GROUP), + "is_ets_member": in_group(_user, ETS_GROUP), + "LANIS_LINK": comp.get_LANIS_link(), + TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}", + "has_finished_deadlines": comp.get_finished_deadlines().exists(), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def remove_view(request: HttpRequest, id: str): + """ Renders a modal view for removing the compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + form = RemoveModalForm(request.POST or None, instance=comp, request=request) + return form.process_request( + request=request, + msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier), + redirect_url=reverse("compensation:index"), + ) + diff --git a/compensation/views/compensation/deadline.py b/compensation/views/compensation/deadline.py new file mode 100644 index 00000000..ee6a5120 --- /dev/null +++ b/compensation/views/compensation/deadline.py @@ -0,0 +1,89 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.deadline import EditDeadlineModalForm, NewDeadlineModalForm +from compensation.models import Compensation +from konova.decorators import shared_access_required, default_group_required +from konova.forms.modals import RemoveDeadlineModalForm +from konova.models import Deadline +from konova.utils.message_templates import DEADLINE_REMOVED, DEADLINE_EDITED, DEADLINE_ADDED + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def deadline_new_view(request: HttpRequest, id: str): + """ Renders a form for adding new states for a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id to which the new state will be related + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + form = NewDeadlineModalForm(request.POST or None, instance=comp, request=request) + return form.process_request( + request, + msg_success=DEADLINE_ADDED, + redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str): + """ Renders a form for editing deadlines from a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + deadline_id (str): The deadline's id + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + deadline = get_object_or_404(Deadline, id=deadline_id) + form = EditDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request) + return form.process_request( + request, + msg_success=DEADLINE_EDITED, + redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): + """ Renders a form for removing deadlines from a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + deadline_id (str): The deadline's id + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + deadline = get_object_or_404(Deadline, id=deadline_id) + form = RemoveDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request) + return form.process_request( + request, + msg_success=DEADLINE_REMOVED, + redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" + ) + diff --git a/compensation/views/compensation/document.py b/compensation/views/compensation/document.py new file mode 100644 index 00000000..8ca99edd --- /dev/null +++ b/compensation/views/compensation/document.py @@ -0,0 +1,111 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.document import NewCompensationDocumentModalForm +from compensation.models import Compensation, CompensationDocument +from konova.decorators import shared_access_required, default_group_required +from konova.forms.modals import EditDocumentModalForm +from konova.utils.documents import remove_document, get_document +from konova.utils.message_templates import DOCUMENT_EDITED, DOCUMENT_ADDED + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def new_document_view(request: HttpRequest, id: str): + """ Renders a form for uploading new documents + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id to which the new document will be related + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + form = NewCompensationDocumentModalForm(request.POST or None, request.FILES or None, instance=comp, request=request) + return form.process_request( + request, + msg_success=DOCUMENT_ADDED, + redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def get_document_view(request: HttpRequest, id: str, doc_id: str): + """ Returns the document as downloadable file + + Wraps the generic document fetcher function from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The compensation id + doc_id (str): The document id + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + doc = get_object_or_404(CompensationDocument, id=doc_id) + return get_document(doc) + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def remove_document_view(request: HttpRequest, id: str, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The compensation id + doc_id (str): The document id + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + doc = get_object_or_404(CompensationDocument, id=doc_id) + return remove_document( + request, + doc + ) + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def edit_document_view(request: HttpRequest, id: str, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The compensation id + doc_id (str): The document id + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + doc = get_object_or_404(CompensationDocument, id=doc_id) + form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=comp, document=doc, request=request) + return form.process_request( + request, + DOCUMENT_EDITED, + reverse("compensation:detail", args=(id,)) + "#related_data" + ) + diff --git a/compensation/views/compensation/log.py b/compensation/views/compensation/log.py new file mode 100644 index 00000000..91b38aa5 --- /dev/null +++ b/compensation/views/compensation/log.py @@ -0,0 +1,41 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404, render +from django.utils.translation import gettext_lazy as _ + +from compensation.models import Compensation +from konova.contexts import BaseContext +from konova.decorators import shared_access_required, default_group_required + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def log_view(request: HttpRequest, id: str): + """ Renders a log view using modal + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + template = "modal/modal_generic.html" + body_template = "log.html" + + context = { + "modal_body_template": body_template, + "log": comp.log.all(), + "modal_title": _("Log"), + } + context = BaseContext(request, context).context + return render(request, template, context) diff --git a/compensation/views/compensation/report.py b/compensation/views/compensation/report.py new file mode 100644 index 00000000..28d563e9 --- /dev/null +++ b/compensation/views/compensation/report.py @@ -0,0 +1,79 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.http import HttpRequest +from django.shortcuts import get_object_or_404, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from compensation.models import Compensation +from konova.contexts import BaseContext +from konova.forms import SimpleGeomForm +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.utils.generators import generate_qr_code + + +def report_view(request: HttpRequest, id: str): + """ Renders the public report view + + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention + + Returns: + + """ + # Reuse the compensation report template since compensations are structurally identical + template = "compensation/report/compensation/report.html" + comp = get_object_or_404(Compensation, id=id) + + tab_title = _("Report {}").format(comp.identifier) + # If intervention is not recorded (yet or currently) we need to render another template without any data + if not comp.is_ready_for_publish(): + template = "report/unavailable.html" + context = { + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) + + # Prepare data for map viewer + geom_form = SimpleGeomForm( + instance=comp + ) + parcels = comp.get_underlying_parcels() + + qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,))) + qrcode_img = generate_qr_code(qrcode_url, 10) + qrcode_lanis_url = comp.get_LANIS_link() + qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) + + # Order states by surface + before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type") + after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type") + actions = comp.actions.all().prefetch_related("action_type") + + context = { + "obj": comp, + "qrcode": { + "img": qrcode_img, + "url": qrcode_url, + }, + "qrcode_lanis": { + "img": qrcode_img_lanis, + "url": qrcode_lanis_url, + }, + "has_access": False, # disables action buttons during rendering + "before_states": before_states, + "after_states": after_states, + "geom_form": geom_form, + "parcels": parcels, + "actions": actions, + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) diff --git a/compensation/views/compensation/resubmission.py b/compensation/views/compensation/resubmission.py new file mode 100644 index 00000000..d90a91ce --- /dev/null +++ b/compensation/views/compensation/resubmission.py @@ -0,0 +1,39 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from compensation.models import Compensation +from konova.decorators import shared_access_required, default_group_required +from konova.forms.modals import ResubmissionModalForm + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def create_resubmission_view(request: HttpRequest, id: str): + """ Renders resubmission form for a compensation + + Args: + request (HttpRequest): The incoming request + id (str): Compensation's id + + Returns: + + """ + com = get_object_or_404(Compensation, id=id) + form = ResubmissionModalForm(request.POST or None, instance=com, request=request) + form.action_url = reverse("compensation:resubmission-create", args=(id,)) + return form.process_request( + request, + msg_success=_("Resubmission set"), + redirect_url=reverse("compensation:detail", args=(id,)) + ) diff --git a/compensation/views/compensation/state.py b/compensation/views/compensation/state.py new file mode 100644 index 00000000..ca8c5d0c --- /dev/null +++ b/compensation/views/compensation/state.py @@ -0,0 +1,89 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.state import EditCompensationStateModalForm, RemoveCompensationStateModalForm, \ + NewCompensationStateModalForm +from compensation.models import Compensation, CompensationState +from konova.decorators import shared_access_required, default_group_required +from konova.utils.message_templates import COMPENSATION_STATE_EDITED, COMPENSATION_STATE_REMOVED, \ + COMPENSATION_STATE_ADDED + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def state_new_view(request: HttpRequest, id: str): + """ Renders a form for adding new states for a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id to which the new state will be related + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + form = NewCompensationStateModalForm(request.POST or None, instance=comp, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_ADDED, + redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def state_remove_view(request: HttpRequest, id: str, state_id: str): + """ Renders a form for removing a compensation state + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + state_id (str): The state's id + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + state = get_object_or_404(CompensationState, id=state_id) + form = RemoveCompensationStateModalForm(request.POST or None, instance=comp, state=state, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_REMOVED, + redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(Compensation, "id") +def state_edit_view(request: HttpRequest, id: str, state_id: str): + """ Renders a form for editing a compensation state + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + state_id (str): The state's id + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + state = get_object_or_404(CompensationState, id=state_id) + form = EditCompensationStateModalForm(request.POST or None, instance=comp, state=state, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_EDITED, + redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" + ) + From d168ec47ce96a9eefef04522a79e2081f1b093b1 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 19 Aug 2022 08:27:42 +0200 Subject: [PATCH 21/48] EcoAccount views * splits compensation/views/eco_account.py (+700 lines) into separate files in new module * view files can now be found in /compensation/views/eco_account/... --- compensation/urls/eco_account.py | 16 +- compensation/views/eco_account.py | 869 ------------------ compensation/views/eco_account/__init__.py | 7 + compensation/views/eco_account/action.py | 89 ++ compensation/views/eco_account/deadline.py | 89 ++ compensation/views/eco_account/deduction.py | 101 ++ compensation/views/eco_account/document.py | 111 +++ compensation/views/eco_account/eco_account.py | 266 ++++++ compensation/views/eco_account/log.py | 42 + compensation/views/eco_account/record.py | 38 + compensation/views/eco_account/report.py | 86 ++ .../views/eco_account/resubmission.py | 39 + compensation/views/eco_account/share.py | 78 ++ compensation/views/eco_account/state.py | 88 ++ 14 files changed, 1048 insertions(+), 871 deletions(-) delete mode 100644 compensation/views/eco_account.py create mode 100644 compensation/views/eco_account/__init__.py create mode 100644 compensation/views/eco_account/action.py create mode 100644 compensation/views/eco_account/deadline.py create mode 100644 compensation/views/eco_account/deduction.py create mode 100644 compensation/views/eco_account/document.py create mode 100644 compensation/views/eco_account/eco_account.py create mode 100644 compensation/views/eco_account/log.py create mode 100644 compensation/views/eco_account/record.py create mode 100644 compensation/views/eco_account/report.py create mode 100644 compensation/views/eco_account/resubmission.py create mode 100644 compensation/views/eco_account/share.py create mode 100644 compensation/views/eco_account/state.py diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index c57540e5..85f74121 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -8,7 +8,19 @@ Created on: 24.08.21 from django.urls import path from compensation.autocomplete.eco_account import EcoAccountAutocomplete -from compensation.views.eco_account import * +from compensation.views.eco_account.eco_account import index_view, new_view, new_id_view, edit_view, remove_view, \ + detail_view +from compensation.views.eco_account.log import log_view +from compensation.views.eco_account.record import record_view +from compensation.views.eco_account.report import report_view +from compensation.views.eco_account.resubmission import create_resubmission_view +from compensation.views.eco_account.state import state_new_view, state_remove_view, state_edit_view +from compensation.views.eco_account.action import action_edit_view, action_new_view, action_remove_view +from compensation.views.eco_account.deadline import deadline_new_view, deadline_edit_view, deadline_remove_view +from compensation.views.eco_account.share import share_view, create_share_view +from compensation.views.eco_account.document import get_document_view, new_document_view, remove_document_view, \ + edit_document_view +from compensation.views.eco_account.deduction import deduction_edit_view, deduction_remove_view, new_deduction_view app_name = "acc" urlpatterns = [ @@ -51,4 +63,4 @@ urlpatterns = [ # Autocomplete path("atcmplt/eco-accounts", EcoAccountAutocomplete.as_view(), name="autocomplete"), -] \ No newline at end of file +] diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py deleted file mode 100644 index 51aa925d..00000000 --- a/compensation/views/eco_account.py +++ /dev/null @@ -1,869 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 09.08.21 - -""" -from django.contrib import messages -from django.db.models import Sum -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ -from django.contrib.auth.decorators import login_required -from django.core.exceptions import ObjectDoesNotExist -from django.http import HttpRequest, Http404, JsonResponse -from django.shortcuts import render, get_object_or_404, redirect - -from compensation.forms.eco_account import NewEcoAccountForm, EditEcoAccountForm -from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \ - RemoveCompensationActionModalForm, EditCompensationActionModalForm -from compensation.forms.modals.deadline import EditDeadlineModalForm, NewDeadlineModalForm -from compensation.forms.modals.document import NewEcoAccountDocumentModalForm -from compensation.forms.modals.state import NewCompensationStateModalForm, RemoveCompensationStateModalForm, \ - EditCompensationStateModalForm -from compensation.models import EcoAccount, EcoAccountDocument, CompensationState, CompensationAction -from compensation.tables.eco_account import EcoAccountTable -from intervention.forms.modals.deduction import RemoveEcoAccountDeductionModalForm, NewEcoAccountDeductionModalForm, \ - EditEcoAccountDeductionModalForm -from intervention.forms.modals.share import ShareModalForm -from konova.contexts import BaseContext -from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \ - shared_access_required -from konova.forms.modals import RemoveModalForm, RecordModalForm, \ - RemoveDeadlineModalForm, EditDocumentModalForm, ResubmissionModalForm -from konova.forms import SimpleGeomForm -from konova.models import Deadline -from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.documents import get_document, remove_document -from konova.utils.generators import generate_qr_code -from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, \ - CANCEL_ACC_RECORDED_OR_DEDUCTED, DEDUCTION_REMOVED, DEDUCTION_ADDED, DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, \ - COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, \ - DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, \ - RECORDED_BLOCKS_EDIT -from konova.utils.user_checks import in_group - - -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ - Renders the index view for eco accounts - - Args: - request (HttpRequest): The incoming request - - Returns: - A rendered view - """ - template = "generic_index.html" - eco_accounts = EcoAccount.objects.filter( - deleted=None, - ) - table = EcoAccountTable( - request=request, - queryset=eco_accounts - ) - context = { - "table": table, - TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -def new_view(request: HttpRequest): - """ - Renders a view for a new eco account creation - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "compensation/form/view.html" - data_form = NewEcoAccountForm(request.POST or None) - geom_form = SimpleGeomForm(request.POST or None, read_only=False) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - generated_identifier = data_form.cleaned_data.get("identifier", None) - acc = data_form.save(request.user, geom_form) - if generated_identifier != acc.identifier: - messages.info( - request, - IDENTIFIER_REPLACED.format( - generated_identifier, - acc.identifier - ) - ) - messages.success(request, _("Eco-Account {} added").format(acc.identifier)) - return redirect("compensation:acc:detail", id=acc.id) - else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("New Eco-Account"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -def new_id_view(request: HttpRequest): - """ JSON endpoint - - Provides fetching of free identifiers for e.g. AJAX calls - - """ - tmp = EcoAccount() - identifier = tmp.generate_new_identifier() - while EcoAccount.objects.filter(identifier=identifier).exists(): - identifier = tmp.generate_new_identifier() - return JsonResponse( - data={ - "gen_data": identifier - } - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing compensations - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "compensation/form/view.html" - # Get object from db - acc = get_object_or_404(EcoAccount, id=id) - if acc.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("compensation:acc:detail", id=id) - - # Create forms, initialize with values from db/from POST request - data_form = EditEcoAccountForm(request.POST or None, instance=acc) - geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - # The data form takes the geom form for processing, as well as the performing user - acc = data_form.save(request.user, geom_form) - messages.success(request, _("Eco-Account {} edited").format(acc.identifier)) - return redirect("compensation:acc:detail", id=acc.id) - else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@any_group_check -def detail_view(request: HttpRequest, id: str): - """ Renders a detail view for a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - - Returns: - - """ - template = "compensation/detail/eco_account/view.html" - acc = get_object_or_404( - EcoAccount.objects.prefetch_related( - "deadlines", - ).select_related( - 'geometry', - 'responsible', - ), - id=id - ) - geom_form = SimpleGeomForm(instance=acc) - parcels = acc.get_underlying_parcels() - _user = request.user - is_data_shared = acc.is_shared_with(_user) - - # Order states according to surface - before_states = acc.before_states.order_by("-surface") - after_states = acc.after_states.order_by("-surface") - - # Precalculate logical errors between before- and after-states - # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling - sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0 - sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 - diff_states = abs(sum_before_states - sum_after_states) - # Calculate rest of available surface for deductions - available_total, available_relative = acc.get_available_rest() - - # Prefetch related data to decrease the amount of db connections - deductions = acc.deductions.filter( - intervention__deleted=None, - ) - actions = acc.actions.all() - - request = acc.set_status_messages(request) - - context = { - "obj": acc, - "geom_form": geom_form, - "parcels": parcels, - "has_access": is_data_shared, - "before_states": before_states, - "after_states": after_states, - "sum_before_states": sum_before_states, - "sum_after_states": sum_after_states, - "diff_states": diff_states, - "available": available_relative, - "available_total": available_total, - "is_default_member": in_group(_user, DEFAULT_GROUP), - "is_zb_member": in_group(_user, ZB_GROUP), - "is_ets_member": in_group(_user, ETS_GROUP), - "LANIS_LINK": acc.get_LANIS_link(), - "deductions": deductions, - "actions": actions, - TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}", - "has_finished_deadlines": acc.get_finished_deadlines().exists(), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def remove_view(request: HttpRequest, id: str): - """ Renders a modal view for removing the eco account - - Args: - request (HttpRequest): The incoming request - id (str): The account's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - - # If the eco account has already been recorded OR there are already deductions, it can not be deleted by a regular - # default group user - if acc.recorded is not None or acc.deductions.exists(): - user = request.user - if not in_group(user, ETS_GROUP): - messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED) - return redirect("compensation:acc:detail", id=id) - - form = RemoveModalForm(request.POST or None, instance=acc, request=request) - return form.process_request( - request=request, - msg_success=_("Eco-account removed"), - redirect_url=reverse("compensation:acc:index"), - ) - - -@login_required -@default_group_required -def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str): - """ Renders a modal view for removing deductions - - Args: - request (HttpRequest): The incoming request - id (str): The eco account's id - deduction_id (str): The deduction's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - try: - eco_deduction = acc.deductions.get(id=deduction_id) - if not eco_deduction.intervention.is_shared_with(request.user): - raise ObjectDoesNotExist() - except ObjectDoesNotExist: - raise Http404("Unknown deduction") - - form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=acc, deduction=eco_deduction, request=request) - return form.process_request( - request=request, - msg_success=DEDUCTION_REMOVED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -def deduction_edit_view(request: HttpRequest, id: str, deduction_id: str): - """ Renders a modal view for editing deductions - - Args: - request (HttpRequest): The incoming request - id (str): The eco account's id - deduction_id (str): The deduction's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - try: - eco_deduction = acc.deductions.get(id=deduction_id) - if not eco_deduction.intervention.is_shared_with(request.user): - raise ObjectDoesNotExist - except ObjectDoesNotExist: - raise Http404("Unknown deduction") - - form = EditEcoAccountDeductionModalForm(request.POST or None, instance=acc, deduction=eco_deduction, request=request) - return form.process_request( - request=request, - msg_success=DEDUCTION_EDITED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def log_view(request: HttpRequest, id: str): - """ Renders a log view using modal - - Args: - request (HttpRequest): The incoming request - id (str): The eco acount's id - - Returns: - - """ - comp = get_object_or_404(EcoAccount, id=id) - template = "modal/modal_generic.html" - body_template = "log.html" - - context = { - "modal_body_template": body_template, - "log": comp.log.all(), - "modal_title": _("Log"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@conservation_office_group_required -@shared_access_required(EcoAccount, "id") -def record_view(request: HttpRequest, id:str): - """ Renders a modal form for recording an eco account - - Args: - request (HttpRequest): The incoming request - id (str): The account's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - form = RecordModalForm(request.POST or None, instance=acc, request=request) - msg_succ = _("{} unrecorded") if acc.recorded else _("{} recorded") - msg_succ = msg_succ.format(acc.identifier) - return form.process_request( - request, - msg_succ - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def state_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for an eco account - - Args: - request (HttpRequest): The incoming request - id (str): The account's id to which the new state will be related - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - form = NewCompensationStateModalForm(request.POST or None, instance=acc, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_ADDED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def action_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new actions for an eco account - - Args: - request (HttpRequest): The incoming request - id (str): The account's id to which the new state will be related - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - form = NewCompensationActionModalForm(request.POST or None, instance=acc, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_ADDED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def state_remove_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for removing a compensation state - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - state_id (str): The state's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = RemoveCompensationStateModalForm(request.POST or None, instance=acc, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_REMOVED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def state_edit_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for editing a compensation state - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - state_id (str): The state's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = EditCompensationStateModalForm(request.POST or None, instance=acc, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_EDITED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def action_remove_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for removing a compensation action - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - id (str): The action's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = RemoveCompensationActionModalForm(request.POST or None, instance=acc, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_REMOVED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def action_edit_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for editing a compensation action - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - id (str): The action's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = EditCompensationActionModalForm(request.POST or None, instance=acc, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_EDITED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for editing deadlines from a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - comp = get_object_or_404(EcoAccount, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = EditDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_EDITED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for removing deadlines from a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - comp = get_object_or_404(EcoAccount, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = RemoveDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_REMOVED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def deadline_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for an eco account - - Args: - request (HttpRequest): The incoming request - id (str): The account's id to which the new state will be related - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - form = NewDeadlineModalForm(request.POST or None, instance=acc, request=request) - return form.process_request( - request, - msg_success=DEADLINE_ADDED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def new_document_view(request: HttpRequest, id: str): - """ Renders a form for uploading new documents - - Args: - request (HttpRequest): The incoming request - id (str): The account's id to which the new document will be related - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - form = NewEcoAccountDocumentModalForm(request.POST or None, request.FILES or None, instance=acc, request=request) - return form.process_request( - request, - msg_success=DOCUMENT_ADDED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data", - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def get_document_view(request: HttpRequest, id:str, doc_id: str): - """ Returns the document as downloadable file - - Wraps the generic document fetcher function from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The account id - doc_id (str): The document id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - doc = get_object_or_404(EcoAccountDocument, id=doc_id) - return get_document(doc) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def edit_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The account id - doc_id (str): The document id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - doc = get_object_or_404(EcoAccountDocument, id=doc_id) - form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=acc, document=doc, request=request) - return form.process_request( - request, - DOCUMENT_EDITED, - reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def remove_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The account id - doc_id (str): The document id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - doc = get_object_or_404(EcoAccountDocument, id=doc_id) - return remove_document( - request, - doc - ) - - -@login_required -@default_group_required -def new_deduction_view(request: HttpRequest, id: str): - """ Renders a modal form view for creating deductions - - Args: - request (HttpRequest): THe incoming request - id (str): The eco account's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - if not acc.recorded: - raise Http404() - form = NewEcoAccountDeductionModalForm(request.POST or None, instance=acc, request=request) - return form.process_request( - request, - msg_success=DEDUCTION_ADDED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - - -def report_view(request:HttpRequest, id: str): - """ Renders the public report view - - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention - - Returns: - - """ - # Reuse the compensation report template since EcoAccounts are structurally identical - template = "compensation/report/eco_account/report.html" - acc = get_object_or_404(EcoAccount, id=id) - - tab_title = _("Report {}").format(acc.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not acc.is_ready_for_publish(): - template = "report/unavailable.html" - context = { - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) - - # Prepare data for map viewer - geom_form = SimpleGeomForm( - instance=acc - ) - parcels = acc.get_underlying_parcels() - - qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,))) - qrcode_img = generate_qr_code(qrcode_url, 10) - qrcode_lanis_url = acc.get_LANIS_link() - qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) - - # Order states by surface - before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent") - after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent") - actions = acc.actions.all().prefetch_related("action_type__parent") - - # Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier) - deductions = acc.deductions.all()\ - .distinct("intervention")\ - .select_related("intervention")\ - .values_list("intervention__id", "intervention__identifier", "intervention__title", named=True) - - context = { - "obj": acc, - "qrcode": { - "img": qrcode_img, - "url": qrcode_url, - }, - "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url, - }, - "has_access": False, # disables action buttons during rendering - "before_states": before_states, - "after_states": after_states, - "geom_form": geom_form, - "parcels": parcels, - "actions": actions, - "deductions": deductions, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -def share_view(request: HttpRequest, id: str, token: str): - """ Performs sharing of an eco account - - If token given in url is not valid, the user will be redirected to the dashboard - - Args: - request (HttpRequest): The incoming request - id (str): EcoAccount's id - token (str): Access token for EcoAccount - - Returns: - - """ - user = request.user - obj = get_object_or_404(EcoAccount, id=id) - # Check tokens - if obj.access_token == token: - # Send different messages in case user has already been added to list of sharing users - if obj.is_shared_with(user): - messages.info( - request, - _("{} has already been shared with you").format(obj.identifier) - ) - else: - messages.success( - request, - _("{} has been shared with you").format(obj.identifier) - ) - obj.share_with_user(user) - return redirect("compensation:acc:detail", id=id) - else: - messages.error( - request, - _("Share link invalid"), - extra_tags="danger", - ) - return redirect("home") - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def create_share_view(request: HttpRequest, id: str): - """ Renders sharing form for an eco account - - Args: - request (HttpRequest): The incoming request - id (str): EcoAccount's id - - Returns: - - """ - obj = get_object_or_404(EcoAccount, id=id) - form = ShareModalForm(request.POST or None, instance=obj, request=request) - return form.process_request( - request, - msg_success=_("Share settings updated") - ) - - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def create_resubmission_view(request: HttpRequest, id: str): - """ Renders resubmission form for an eco account - - Args: - request (HttpRequest): The incoming request - id (str): EcoAccount's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - form = ResubmissionModalForm(request.POST or None, instance=acc, request=request) - form.action_url = reverse("compensation:acc:resubmission-create", args=(id,)) - return form.process_request( - request, - msg_success=_("Resubmission set"), - redirect_url=reverse("compensation:acc:detail", args=(id,)) - ) \ No newline at end of file diff --git a/compensation/views/eco_account/__init__.py b/compensation/views/eco_account/__init__.py new file mode 100644 index 00000000..3a996e1f --- /dev/null +++ b/compensation/views/eco_account/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" diff --git a/compensation/views/eco_account/action.py b/compensation/views/eco_account/action.py new file mode 100644 index 00000000..c7eb2185 --- /dev/null +++ b/compensation/views/eco_account/action.py @@ -0,0 +1,89 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.compensation_action import EditCompensationActionModalForm, \ + RemoveCompensationActionModalForm, NewCompensationActionModalForm +from compensation.models import EcoAccount, CompensationAction +from konova.decorators import shared_access_required, default_group_required +from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, COMPENSATION_ACTION_REMOVED, \ + COMPENSATION_ACTION_ADDED + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def action_new_view(request: HttpRequest, id: str): + """ Renders a form for adding new actions for an eco account + + Args: + request (HttpRequest): The incoming request + id (str): The account's id to which the new state will be related + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + form = NewCompensationActionModalForm(request.POST or None, instance=acc, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_ACTION_ADDED, + redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def action_remove_view(request: HttpRequest, id: str, action_id: str): + """ Renders a form for removing a compensation action + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + id (str): The action's id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + action = get_object_or_404(CompensationAction, id=action_id) + form = RemoveCompensationActionModalForm(request.POST or None, instance=acc, action=action, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_ACTION_REMOVED, + redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def action_edit_view(request: HttpRequest, id: str, action_id: str): + """ Renders a form for editing a compensation action + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + id (str): The action's id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + action = get_object_or_404(CompensationAction, id=action_id) + form = EditCompensationActionModalForm(request.POST or None, instance=acc, action=action, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_ACTION_EDITED, + redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" + ) + diff --git a/compensation/views/eco_account/deadline.py b/compensation/views/eco_account/deadline.py new file mode 100644 index 00000000..37e35202 --- /dev/null +++ b/compensation/views/eco_account/deadline.py @@ -0,0 +1,89 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm +from compensation.models import EcoAccount +from konova.decorators import shared_access_required, default_group_required +from konova.forms.modals import RemoveDeadlineModalForm +from konova.models import Deadline +from konova.utils.message_templates import DEADLINE_ADDED, DEADLINE_REMOVED, DEADLINE_EDITED + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str): + """ Renders a form for editing deadlines from a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + deadline_id (str): The deadline's id + + Returns: + + """ + comp = get_object_or_404(EcoAccount, id=id) + deadline = get_object_or_404(Deadline, id=deadline_id) + form = EditDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request) + return form.process_request( + request, + msg_success=DEADLINE_EDITED, + redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): + """ Renders a form for removing deadlines from a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + deadline_id (str): The deadline's id + + Returns: + + """ + comp = get_object_or_404(EcoAccount, id=id) + deadline = get_object_or_404(Deadline, id=deadline_id) + form = RemoveDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request) + return form.process_request( + request, + msg_success=DEADLINE_REMOVED, + redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def deadline_new_view(request: HttpRequest, id: str): + """ Renders a form for adding new states for an eco account + + Args: + request (HttpRequest): The incoming request + id (str): The account's id to which the new state will be related + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + form = NewDeadlineModalForm(request.POST or None, instance=acc, request=request) + return form.process_request( + request, + msg_success=DEADLINE_ADDED, + redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" + ) + diff --git a/compensation/views/eco_account/deduction.py b/compensation/views/eco_account/deduction.py new file mode 100644 index 00000000..e9b1dcb9 --- /dev/null +++ b/compensation/views/eco_account/deduction.py @@ -0,0 +1,101 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.core.exceptions import ObjectDoesNotExist +from django.http import HttpRequest, Http404 +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.models import EcoAccount +from intervention.forms.modals.deduction import EditEcoAccountDeductionModalForm, RemoveEcoAccountDeductionModalForm, \ + NewEcoAccountDeductionModalForm +from konova.decorators import default_group_required +from konova.utils.message_templates import DEDUCTION_EDITED, DEDUCTION_REMOVED, DEDUCTION_ADDED + + +@login_required +@default_group_required +def new_deduction_view(request: HttpRequest, id: str): + """ Renders a modal form view for creating deductions + + Args: + request (HttpRequest): THe incoming request + id (str): The eco account's id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + if not acc.recorded: + raise Http404() + form = NewEcoAccountDeductionModalForm(request.POST or None, instance=acc, request=request) + return form.process_request( + request, + msg_success=DEDUCTION_ADDED, + redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str): + """ Renders a modal view for removing deductions + + Args: + request (HttpRequest): The incoming request + id (str): The eco account's id + deduction_id (str): The deduction's id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + try: + eco_deduction = acc.deductions.get(id=deduction_id) + if not eco_deduction.intervention.is_shared_with(request.user): + raise ObjectDoesNotExist() + except ObjectDoesNotExist: + raise Http404("Unknown deduction") + + form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=acc, deduction=eco_deduction, request=request) + return form.process_request( + request=request, + msg_success=DEDUCTION_REMOVED, + redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +def deduction_edit_view(request: HttpRequest, id: str, deduction_id: str): + """ Renders a modal view for editing deductions + + Args: + request (HttpRequest): The incoming request + id (str): The eco account's id + deduction_id (str): The deduction's id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + try: + eco_deduction = acc.deductions.get(id=deduction_id) + if not eco_deduction.intervention.is_shared_with(request.user): + raise ObjectDoesNotExist + except ObjectDoesNotExist: + raise Http404("Unknown deduction") + + form = EditEcoAccountDeductionModalForm(request.POST or None, instance=acc, deduction=eco_deduction, request=request) + return form.process_request( + request=request, + msg_success=DEDUCTION_EDITED, + redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" + ) + + diff --git a/compensation/views/eco_account/document.py b/compensation/views/eco_account/document.py new file mode 100644 index 00000000..53e0a200 --- /dev/null +++ b/compensation/views/eco_account/document.py @@ -0,0 +1,111 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.document import NewEcoAccountDocumentModalForm +from compensation.models import EcoAccount, EcoAccountDocument +from konova.decorators import shared_access_required, default_group_required +from konova.forms.modals import EditDocumentModalForm +from konova.utils.documents import remove_document, get_document +from konova.utils.message_templates import DOCUMENT_EDITED, DOCUMENT_ADDED + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def new_document_view(request: HttpRequest, id: str): + """ Renders a form for uploading new documents + + Args: + request (HttpRequest): The incoming request + id (str): The account's id to which the new document will be related + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + form = NewEcoAccountDocumentModalForm(request.POST or None, request.FILES or None, instance=acc, request=request) + return form.process_request( + request, + msg_success=DOCUMENT_ADDED, + redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data", + ) + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def get_document_view(request: HttpRequest, id:str, doc_id: str): + """ Returns the document as downloadable file + + Wraps the generic document fetcher function from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The account id + doc_id (str): The document id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + doc = get_object_or_404(EcoAccountDocument, id=doc_id) + return get_document(doc) + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def edit_document_view(request: HttpRequest, id: str, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The account id + doc_id (str): The document id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + doc = get_object_or_404(EcoAccountDocument, id=doc_id) + form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=acc, document=doc, request=request) + return form.process_request( + request, + DOCUMENT_EDITED, + reverse("compensation:acc:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def remove_document_view(request: HttpRequest, id: str, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The account id + doc_id (str): The document id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + doc = get_object_or_404(EcoAccountDocument, id=doc_id) + return remove_document( + request, + doc + ) + diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py new file mode 100644 index 00000000..df99b957 --- /dev/null +++ b/compensation/views/eco_account/eco_account.py @@ -0,0 +1,266 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.db.models import Sum +from django.http import HttpRequest, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm +from compensation.models import EcoAccount +from compensation.tables.eco_account import EcoAccountTable +from konova.contexts import BaseContext +from konova.decorators import shared_access_required, default_group_required, any_group_check +from konova.forms import SimpleGeomForm +from konova.forms.modals import RemoveModalForm +from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID +from konova.utils.user_checks import in_group + + +@login_required +@any_group_check +def index_view(request: HttpRequest): + """ + Renders the index view for eco accounts + + Args: + request (HttpRequest): The incoming request + + Returns: + A rendered view + """ + template = "generic_index.html" + eco_accounts = EcoAccount.objects.filter( + deleted=None, + ) + table = EcoAccountTable( + request=request, + queryset=eco_accounts + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@default_group_required +def new_view(request: HttpRequest): + """ + Renders a view for a new eco account creation + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + template = "compensation/form/view.html" + data_form = NewEcoAccountForm(request.POST or None) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) + if request.method == "POST": + if data_form.is_valid() and geom_form.is_valid(): + generated_identifier = data_form.cleaned_data.get("identifier", None) + acc = data_form.save(request.user, geom_form) + if generated_identifier != acc.identifier: + messages.info( + request, + IDENTIFIER_REPLACED.format( + generated_identifier, + acc.identifier + ) + ) + messages.success(request, _("Eco-Account {} added").format(acc.identifier)) + return redirect("compensation:acc:detail", id=acc.id) + else: + messages.error(request, FORM_INVALID, extra_tags="danger",) + else: + # For clarification: nothing in this case + pass + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New Eco-Account"), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@default_group_required +def new_id_view(request: HttpRequest): + """ JSON endpoint + + Provides fetching of free identifiers for e.g. AJAX calls + + """ + tmp = EcoAccount() + identifier = tmp.generate_new_identifier() + while EcoAccount.objects.filter(identifier=identifier).exists(): + identifier = tmp.generate_new_identifier() + return JsonResponse( + data={ + "gen_data": identifier + } + ) + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def edit_view(request: HttpRequest, id: str): + """ + Renders a view for editing compensations + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + template = "compensation/form/view.html" + # Get object from db + acc = get_object_or_404(EcoAccount, id=id) + if acc.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("compensation:acc:detail", id=id) + + # Create forms, initialize with values from db/from POST request + data_form = EditEcoAccountForm(request.POST or None, instance=acc) + geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc) + if request.method == "POST": + if data_form.is_valid() and geom_form.is_valid(): + # The data form takes the geom form for processing, as well as the performing user + acc = data_form.save(request.user, geom_form) + messages.success(request, _("Eco-Account {} edited").format(acc.identifier)) + return redirect("compensation:acc:detail", id=acc.id) + else: + messages.error(request, FORM_INVALID, extra_tags="danger",) + else: + # For clarification: nothing in this case + pass + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@any_group_check +def detail_view(request: HttpRequest, id: str): + """ Renders a detail view for a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + + Returns: + + """ + template = "compensation/detail/eco_account/view.html" + acc = get_object_or_404( + EcoAccount.objects.prefetch_related( + "deadlines", + ).select_related( + 'geometry', + 'responsible', + ), + id=id + ) + geom_form = SimpleGeomForm(instance=acc) + parcels = acc.get_underlying_parcels() + _user = request.user + is_data_shared = acc.is_shared_with(_user) + + # Order states according to surface + before_states = acc.before_states.order_by("-surface") + after_states = acc.after_states.order_by("-surface") + + # Precalculate logical errors between before- and after-states + # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling + sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0 + sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 + diff_states = abs(sum_before_states - sum_after_states) + # Calculate rest of available surface for deductions + available_total, available_relative = acc.get_available_rest() + + # Prefetch related data to decrease the amount of db connections + deductions = acc.deductions.filter( + intervention__deleted=None, + ) + actions = acc.actions.all() + + request = acc.set_status_messages(request) + + context = { + "obj": acc, + "geom_form": geom_form, + "parcels": parcels, + "has_access": is_data_shared, + "before_states": before_states, + "after_states": after_states, + "sum_before_states": sum_before_states, + "sum_after_states": sum_after_states, + "diff_states": diff_states, + "available": available_relative, + "available_total": available_total, + "is_default_member": in_group(_user, DEFAULT_GROUP), + "is_zb_member": in_group(_user, ZB_GROUP), + "is_ets_member": in_group(_user, ETS_GROUP), + "LANIS_LINK": acc.get_LANIS_link(), + "deductions": deductions, + "actions": actions, + TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}", + "has_finished_deadlines": acc.get_finished_deadlines().exists(), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def remove_view(request: HttpRequest, id: str): + """ Renders a modal view for removing the eco account + + Args: + request (HttpRequest): The incoming request + id (str): The account's id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + + # If the eco account has already been recorded OR there are already deductions, it can not be deleted by a regular + # default group user + if acc.recorded is not None or acc.deductions.exists(): + user = request.user + if not in_group(user, ETS_GROUP): + messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED) + return redirect("compensation:acc:detail", id=id) + + form = RemoveModalForm(request.POST or None, instance=acc, request=request) + return form.process_request( + request=request, + msg_success=_("Eco-account removed"), + redirect_url=reverse("compensation:acc:index"), + ) + diff --git a/compensation/views/eco_account/log.py b/compensation/views/eco_account/log.py new file mode 100644 index 00000000..0a795199 --- /dev/null +++ b/compensation/views/eco_account/log.py @@ -0,0 +1,42 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404, render +from django.utils.translation import gettext_lazy as _ + +from compensation.models import EcoAccount +from konova.contexts import BaseContext +from konova.decorators import shared_access_required, default_group_required + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def log_view(request: HttpRequest, id: str): + """ Renders a log view using modal + + Args: + request (HttpRequest): The incoming request + id (str): The eco acount's id + + Returns: + + """ + comp = get_object_or_404(EcoAccount, id=id) + template = "modal/modal_generic.html" + body_template = "log.html" + + context = { + "modal_body_template": body_template, + "log": comp.log.all(), + "modal_title": _("Log"), + } + context = BaseContext(request, context).context + return render(request, template, context) + diff --git a/compensation/views/eco_account/record.py b/compensation/views/eco_account/record.py new file mode 100644 index 00000000..c1fe1384 --- /dev/null +++ b/compensation/views/eco_account/record.py @@ -0,0 +1,38 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.utils.translation import gettext_lazy as _ + +from compensation.models import EcoAccount +from konova.decorators import shared_access_required, conservation_office_group_required +from konova.forms.modals import RecordModalForm + + +@login_required +@conservation_office_group_required +@shared_access_required(EcoAccount, "id") +def record_view(request: HttpRequest, id:str): + """ Renders a modal form for recording an eco account + + Args: + request (HttpRequest): The incoming request + id (str): The account's id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + form = RecordModalForm(request.POST or None, instance=acc, request=request) + msg_succ = _("{} unrecorded") if acc.recorded else _("{} recorded") + msg_succ = msg_succ.format(acc.identifier) + return form.process_request( + request, + msg_succ + ) diff --git a/compensation/views/eco_account/report.py b/compensation/views/eco_account/report.py new file mode 100644 index 00000000..9bbf32b1 --- /dev/null +++ b/compensation/views/eco_account/report.py @@ -0,0 +1,86 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.http import HttpRequest +from django.shortcuts import get_object_or_404, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from compensation.models import EcoAccount +from konova.contexts import BaseContext +from konova.forms import SimpleGeomForm +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.utils.generators import generate_qr_code + + +def report_view(request: HttpRequest, id: str): + """ Renders the public report view + + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention + + Returns: + + """ + # Reuse the compensation report template since EcoAccounts are structurally identical + template = "compensation/report/eco_account/report.html" + acc = get_object_or_404(EcoAccount, id=id) + + tab_title = _("Report {}").format(acc.identifier) + # If intervention is not recorded (yet or currently) we need to render another template without any data + if not acc.is_ready_for_publish(): + template = "report/unavailable.html" + context = { + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) + + # Prepare data for map viewer + geom_form = SimpleGeomForm( + instance=acc + ) + parcels = acc.get_underlying_parcels() + + qrcode_url = request.build_absolute_uri(reverse("compensation:acc:report", args=(id,))) + qrcode_img = generate_qr_code(qrcode_url, 10) + qrcode_lanis_url = acc.get_LANIS_link() + qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) + + # Order states by surface + before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent") + after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent") + actions = acc.actions.all().prefetch_related("action_type__parent") + + # Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier) + deductions = acc.deductions.all()\ + .distinct("intervention")\ + .select_related("intervention")\ + .values_list("intervention__id", "intervention__identifier", "intervention__title", named=True) + + context = { + "obj": acc, + "qrcode": { + "img": qrcode_img, + "url": qrcode_url, + }, + "qrcode_lanis": { + "img": qrcode_img_lanis, + "url": qrcode_lanis_url, + }, + "has_access": False, # disables action buttons during rendering + "before_states": before_states, + "after_states": after_states, + "geom_form": geom_form, + "parcels": parcels, + "actions": actions, + "deductions": deductions, + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) diff --git a/compensation/views/eco_account/resubmission.py b/compensation/views/eco_account/resubmission.py new file mode 100644 index 00000000..43c52224 --- /dev/null +++ b/compensation/views/eco_account/resubmission.py @@ -0,0 +1,39 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from compensation.models import EcoAccount +from konova.decorators import shared_access_required, default_group_required +from konova.forms.modals import ResubmissionModalForm + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def create_resubmission_view(request: HttpRequest, id: str): + """ Renders resubmission form for an eco account + + Args: + request (HttpRequest): The incoming request + id (str): EcoAccount's id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + form = ResubmissionModalForm(request.POST or None, instance=acc, request=request) + form.action_url = reverse("compensation:acc:resubmission-create", args=(id,)) + return form.process_request( + request, + msg_success=_("Resubmission set"), + redirect_url=reverse("compensation:acc:detail", args=(id,)) + ) diff --git a/compensation/views/eco_account/share.py b/compensation/views/eco_account/share.py new file mode 100644 index 00000000..7b1916e4 --- /dev/null +++ b/compensation/views/eco_account/share.py @@ -0,0 +1,78 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404, redirect +from django.utils.translation import gettext_lazy as _ + +from compensation.models import EcoAccount +from intervention.forms.modals.share import ShareModalForm +from konova.decorators import shared_access_required, default_group_required + + +@login_required +def share_view(request: HttpRequest, id: str, token: str): + """ Performs sharing of an eco account + + If token given in url is not valid, the user will be redirected to the dashboard + + Args: + request (HttpRequest): The incoming request + id (str): EcoAccount's id + token (str): Access token for EcoAccount + + Returns: + + """ + user = request.user + obj = get_object_or_404(EcoAccount, id=id) + # Check tokens + if obj.access_token == token: + # Send different messages in case user has already been added to list of sharing users + if obj.is_shared_with(user): + messages.info( + request, + _("{} has already been shared with you").format(obj.identifier) + ) + else: + messages.success( + request, + _("{} has been shared with you").format(obj.identifier) + ) + obj.share_with_user(user) + return redirect("compensation:acc:detail", id=id) + else: + messages.error( + request, + _("Share link invalid"), + extra_tags="danger", + ) + return redirect("home") + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def create_share_view(request: HttpRequest, id: str): + """ Renders sharing form for an eco account + + Args: + request (HttpRequest): The incoming request + id (str): EcoAccount's id + + Returns: + + """ + obj = get_object_or_404(EcoAccount, id=id) + form = ShareModalForm(request.POST or None, instance=obj, request=request) + return form.process_request( + request, + msg_success=_("Share settings updated") + ) + diff --git a/compensation/views/eco_account/state.py b/compensation/views/eco_account/state.py new file mode 100644 index 00000000..15590fc7 --- /dev/null +++ b/compensation/views/eco_account/state.py @@ -0,0 +1,88 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.state import EditCompensationStateModalForm, RemoveCompensationStateModalForm, \ + NewCompensationStateModalForm +from compensation.models import EcoAccount, CompensationState +from konova.decorators import shared_access_required, default_group_required +from konova.utils.message_templates import COMPENSATION_STATE_EDITED, COMPENSATION_STATE_REMOVED, \ + COMPENSATION_STATE_ADDED + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def state_new_view(request: HttpRequest, id: str): + """ Renders a form for adding new states for an eco account + + Args: + request (HttpRequest): The incoming request + id (str): The account's id to which the new state will be related + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + form = NewCompensationStateModalForm(request.POST or None, instance=acc, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_ADDED, + redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" + ) + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def state_remove_view(request: HttpRequest, id: str, state_id: str): + """ Renders a form for removing a compensation state + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + state_id (str): The state's id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + state = get_object_or_404(CompensationState, id=state_id) + form = RemoveCompensationStateModalForm(request.POST or None, instance=acc, state=state, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_REMOVED, + redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@default_group_required +@shared_access_required(EcoAccount, "id") +def state_edit_view(request: HttpRequest, id: str, state_id: str): + """ Renders a form for editing a compensation state + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + state_id (str): The state's id + + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + state = get_object_or_404(CompensationState, id=state_id) + form = EditCompensationStateModalForm(request.POST or None, instance=acc, state=state, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_EDITED, + redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" + ) + From 3c416fa26401203e9b0023798c7a86ab021e395a Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 19 Aug 2022 10:25:27 +0200 Subject: [PATCH 22/48] Konova views * splits konova/views.py into separate files in new module * view files can now be found in /konova/views/... * introduces first class based view AbstractLogView * implemented for Ema, Intervention, Compensation and EcoAccount --- compensation/urls/compensation.py | 4 +- compensation/urls/eco_account.py | 4 +- compensation/views/compensation/log.py | 36 +--- compensation/views/eco_account/log.py | 37 +--- ema/urls.py | 4 +- ema/views/log.py | 36 +--- intervention/urls.py | 4 +- intervention/views/log.py | 36 +--- konova/urls.py | 9 +- konova/views.py | 248 ------------------------- konova/views/__init__.py | 7 + konova/views/error.py | 38 ++++ konova/views/geometry.py | 108 +++++++++++ konova/views/home.py | 82 ++++++++ konova/views/log.py | 41 ++++ konova/views/logout.py | 26 +++ konova/views/map_proxy.py | 35 ++++ 17 files changed, 387 insertions(+), 368 deletions(-) delete mode 100644 konova/views.py create mode 100644 konova/views/__init__.py create mode 100644 konova/views/error.py create mode 100644 konova/views/geometry.py create mode 100644 konova/views/home.py create mode 100644 konova/views/log.py create mode 100644 konova/views/logout.py create mode 100644 konova/views/map_proxy.py diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index 75ec9297..5b27906e 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -16,7 +16,7 @@ from compensation.views.compensation.action import action_edit_view, action_new_ from compensation.views.compensation.state import state_new_view, state_remove_view, state_edit_view from compensation.views.compensation.compensation import index_view, new_view, new_id_view, detail_view, edit_view, \ remove_view -from compensation.views.compensation.log import log_view +from compensation.views.compensation.log import CompensationLogView urlpatterns = [ # Main compensation @@ -25,7 +25,7 @@ urlpatterns = [ path('new/', new_view, name='new'), path('new', new_view, name='new'), path('', detail_view, name='detail'), - path('/log', log_view, name='log'), + path('/log', CompensationLogView.as_view(), name='log'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index 85f74121..e15dd58f 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -10,7 +10,7 @@ from django.urls import path from compensation.autocomplete.eco_account import EcoAccountAutocomplete from compensation.views.eco_account.eco_account import index_view, new_view, new_id_view, edit_view, remove_view, \ detail_view -from compensation.views.eco_account.log import log_view +from compensation.views.eco_account.log import EcoAccountLogView from compensation.views.eco_account.record import record_view from compensation.views.eco_account.report import report_view from compensation.views.eco_account.resubmission import create_resubmission_view @@ -28,7 +28,7 @@ urlpatterns = [ path('new/', new_view, name='new'), path('new/id', new_id_view, name='new-id'), path('', detail_view, name='detail'), - path('/log', log_view, name='log'), + path('/log', EcoAccountLogView.as_view(), name='log'), path('/record', record_view, name='record'), path('/report', report_view, name='report'), path('/edit', edit_view, name='edit'), diff --git a/compensation/views/compensation/log.py b/compensation/views/compensation/log.py index 91b38aa5..22406843 100644 --- a/compensation/views/compensation/log.py +++ b/compensation/views/compensation/log.py @@ -6,36 +6,18 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, render -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from compensation.models import Compensation -from konova.contexts import BaseContext from konova.decorators import shared_access_required, default_group_required +from konova.views.log import AbstractLogView -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def log_view(request: HttpRequest, id: str): - """ Renders a log view using modal +class CompensationLogView(AbstractLogView): + model = Compensation - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - template = "modal/modal_generic.html" - body_template = "log.html" - - context = { - "modal_body_template": body_template, - "log": comp.log.all(), - "modal_title": _("Log"), - } - context = BaseContext(request, context).context - return render(request, template, context) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/compensation/views/eco_account/log.py b/compensation/views/eco_account/log.py index 0a795199..b9ca5bc3 100644 --- a/compensation/views/eco_account/log.py +++ b/compensation/views/eco_account/log.py @@ -6,37 +6,18 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, render -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from compensation.models import EcoAccount -from konova.contexts import BaseContext from konova.decorators import shared_access_required, default_group_required +from konova.views.log import AbstractLogView -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def log_view(request: HttpRequest, id: str): - """ Renders a log view using modal - - Args: - request (HttpRequest): The incoming request - id (str): The eco acount's id - - Returns: - - """ - comp = get_object_or_404(EcoAccount, id=id) - template = "modal/modal_generic.html" - body_template = "log.html" - - context = { - "modal_body_template": body_template, - "log": comp.log.all(), - "modal_title": _("Log"), - } - context = BaseContext(request, context).context - return render(request, template, context) +class EcoAccountLogView(AbstractLogView): + model = EcoAccount + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/ema/urls.py b/ema/urls.py index 92877271..d4972557 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -11,7 +11,7 @@ from ema.views.action import action_new_view, action_edit_view, action_remove_vi from ema.views.deadline import deadline_new_view, deadline_edit_view, deadline_remove_view from ema.views.document import document_new_view, get_document_view, remove_document_view, edit_document_view from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_view, remove_view -from ema.views.log import log_view +from ema.views.log import EmaLogView from ema.views.record import record_view from ema.views.report import report_view from ema.views.resubmission import create_resubmission_view @@ -24,7 +24,7 @@ urlpatterns = [ path("new/", new_view, name="new"), path("new/id", new_id_view, name="new-id"), path("", detail_view, name="detail"), - path('/log', log_view, name='log'), + path('/log', EmaLogView.as_view(), name='log'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), path('/record', record_view, name='record'), diff --git a/ema/views/log.py b/ema/views/log.py index 3e3f869f..80b4a9ff 100644 --- a/ema/views/log.py +++ b/ema/views/log.py @@ -6,36 +6,18 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, render -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from ema.models import Ema -from konova.contexts import BaseContext from konova.decorators import shared_access_required, conservation_office_group_required +from konova.views.log import AbstractLogView -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def log_view(request: HttpRequest, id: str): - """ Renders a log view using modal +class EmaLogView(AbstractLogView): + model = Ema - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - template = "modal/modal_generic.html" - body_template = "log.html" - - context = { - "modal_body_template": body_template, - "log": ema.log.all(), - "modal_title": _("Log"), - } - context = BaseContext(request, context).context - return render(request, template, context) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/intervention/urls.py b/intervention/urls.py index 2d0dfdb5..ed9dbaae 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -13,7 +13,7 @@ from intervention.views.compensation import remove_compensation_view from intervention.views.deduction import new_deduction_view, edit_deduction_view, remove_deduction_view from intervention.views.document import new_document_view, get_document_view, remove_document_view, edit_document_view from intervention.views.intervention import index_view, new_view, new_id_view, detail_view, edit_view, remove_view -from intervention.views.log import log_view +from intervention.views.log import InterventionLogView from intervention.views.record import record_view from intervention.views.report import report_view from intervention.views.resubmission import create_resubmission_view @@ -27,7 +27,7 @@ urlpatterns = [ path('new/', new_view, name='new'), path('new/id', new_id_view, name='new-id'), path('', detail_view, name='detail'), - path('/log', log_view, name='log'), + path('/log', InterventionLogView.as_view(), name='log'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), path('/share/', share_view, name='share'), diff --git a/intervention/views/log.py b/intervention/views/log.py index fcb69197..709829c6 100644 --- a/intervention/views/log.py +++ b/intervention/views/log.py @@ -6,36 +6,18 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, render -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from intervention.models import Intervention -from konova.contexts import BaseContext from konova.decorators import shared_access_required, default_group_required +from konova.views.log import AbstractLogView -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def log_view(request: HttpRequest, id: str): - """ Renders a log view using modal +class InterventionLogView(AbstractLogView): + model = Intervention - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - template = "modal/modal_generic.html" - body_template = "log.html" - - context = { - "modal_body_template": body_template, - "log": intervention.log.all(), - "modal_title": _("Log"), - } - context = BaseContext(request, context).context - return render(request, template, context) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/konova/urls.py b/konova/urls.py index 1909091e..a8c2426a 100644 --- a/konova/urls.py +++ b/konova/urls.py @@ -19,7 +19,10 @@ from django.urls import path, include from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG from konova.sso.sso import KonovaSSOClient -from konova.views import logout_view, home_view, get_geom_parcels, get_geom_parcels_content, map_client_proxy_view +from konova.views.logout import logout_view +from konova.views.geometry import get_geom_parcels, get_geom_parcels_content +from konova.views.home import home_view +from konova.views.map_proxy import map_client_proxy_view sso_client = KonovaSSOClient(SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY) urlpatterns = [ @@ -45,5 +48,5 @@ if DEBUG: path('__debug__/', include(debug_toolbar.urls)), ] -handler404 = "konova.views.get_404_view" -handler500 = "konova.views.get_500_view" \ No newline at end of file +handler404 = "konova.views.error.get_404_view" +handler500 = "konova.views.error.get_500_view" \ No newline at end of file diff --git a/konova/views.py b/konova/views.py deleted file mode 100644 index 48572a85..00000000 --- a/konova/views.py +++ /dev/null @@ -1,248 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 16.11.20 - -""" -import json - -import requests -from django.contrib.auth import logout -from django.contrib.auth.decorators import login_required -from django.http import HttpRequest, HttpResponse, JsonResponse -from django.shortcuts import redirect, render, get_object_or_404 -from django.template.loader import render_to_string -from django.utils import timezone -from django.utils.translation import gettext_lazy as _ - -from compensation.models import Compensation, EcoAccount -from intervention.models import Intervention -from konova.contexts import BaseContext -from konova.decorators import any_group_check -from konova.models import Deadline, Geometry, Municipal -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from news.models import ServerMessage -from konova.settings import SSO_SERVER_BASE - - -def logout_view(request: HttpRequest): - """ - Logout route for ending the session manually. - - Args: - request (HttpRequest): The used request object - - Returns: - A redirect - """ - logout(request) - return redirect(SSO_SERVER_BASE) - - -@login_required -@any_group_check -def home_view(request: HttpRequest): - """ - Renders the landing page - - Args: - request (HttpRequest): The used request object - - Returns: - A redirect - """ - template = "konova/home.html" - now = timezone.now() - user = request.user - - # Fetch the four newest active and published ServerMessages - msgs = ServerMessage.objects.filter( - is_active=True, - publish_on__lte=now, - unpublish_on__gte=now, - ).order_by( - "-publish_on" - )[:3] - - # First fetch all valid objects (undeleted, only newest versions) - interventions = Intervention.objects.filter( - deleted=None, - ) - # Then fetch only user related ones - user_interventions = interventions.filter( - users__in=[user] - ) - - # Repeat for other objects - comps = Compensation.objects.filter( - deleted=None, - ) - user_comps = comps.filter( - intervention__users__in=[user] - ) - eco_accs = EcoAccount.objects.filter( - deleted=None, - ) - user_ecco_accs = eco_accs.filter( - users__in=[user] - ) - - additional_context = { - "msgs": msgs, - "total_intervention_count": interventions.count(), - "user_intervention_count": user_interventions.count(), - "total_compensation_count": comps.count(), - "user_compensation_count": user_comps.count(), - "total_eco_count": eco_accs.count(), - "user_eco_count": user_ecco_accs.count(), - TAB_TITLE_IDENTIFIER: _("Home"), - } - context = BaseContext(request, additional_context).context - return render(request, template, context) - - -def get_geom_parcels(request: HttpRequest, id: str): - """ Getter for HTMX - - Returns all parcels of the requested geometry rendered into a simple HTML table - - Args: - request (HttpRequest): The incoming request - id (str): The geometry's id - - Returns: - A rendered piece of HTML - """ - # HTTP code 286 states that the HTMX should stop polling for updates - # https://htmx.org/docs/#polling - status_code = 286 - template = "konova/includes/parcels/parcel_table_frame.html" - geom = get_object_or_404(Geometry, id=id) - parcels = geom.get_underlying_parcels() - geos_geom = geom.geom - - parcels_are_currently_calculated = geos_geom is not None and geos_geom.area > 0 and len(parcels) == 0 - parcels_available = len(parcels) > 0 - no_geometry_given = geos_geom is None - - if parcels_are_currently_calculated: - # Parcels are being calculated right now. Change the status code, so polling stays active for fetching - # resutls after the calculation - status_code = 200 - - if parcels_available or no_geometry_given: - parcels = parcels.order_by("-municipal", "flr", "flrstck_zhlr", "flrstck_nnr") - municipals = parcels.order_by("municipal").distinct("municipal").values("municipal__id") - municipals = Municipal.objects.filter(id__in=municipals) - - rpp = 100 - num_all_parcels = parcels.count() - parcels = parcels[:rpp] - next_page = 1 - if len(parcels) < rpp: - next_page = None - - context = { - "num_parcels": num_all_parcels, - "parcels": parcels, - "municipals": municipals, - "geom_id": str(id), - "next_page": next_page, - } - html = render_to_string(template, context, request) - return HttpResponse(html, status=status_code) - else: - return HttpResponse(None, status=404) - - -def get_geom_parcels_content(request: HttpRequest, id: str, page: int): - """ Getter for infinite scroll of HTMX - - Returns parcels of a specific page/slice of the found parcel set. - Implementation of infinite scroll htmx example: https://htmx.org/examples/infinite-scroll/ - - Args: - request (HttpRequest): The incoming request - id (str): The geometry's id - page (int): The requested page number - - Returns: - A rendered piece of HTML - """ - if page < 0: - raise AssertionError("Parcel page can not be negative") - - # HTTP code 286 states that the HTMX should stop polling for updates - # https://htmx.org/docs/#polling - status_code = 286 - template = "konova/includes/parcels/parcel_table_content.html" - geom = get_object_or_404(Geometry, id=id) - parcels = geom.get_underlying_parcels() - - parcels = parcels.order_by("-municipal", "flr", "flrstck_zhlr", "flrstck_nnr") - rpp = 100 - from_p = rpp * (page-1) - to_p = rpp * (page) - next_page = page + 1 - parcels = parcels[from_p:to_p] - if len(parcels) < rpp: - next_page = None - - context = { - "parcels": parcels, - "geom_id": str(id), - "next_page": next_page, - } - html = render_to_string(template, context, request) - return HttpResponse(html, status=status_code) - - -def get_404_view(request: HttpRequest, exception=None): - """ Returns a 404 handling view - - Args: - request (): - exception (): - - Returns: - - """ - context = BaseContext.context - return render(request, "404.html", context, status=404) - - -def get_500_view(request: HttpRequest): - """ Returns a 404 handling view - - Args: - request (): - - Returns: - - """ - context = BaseContext.context - return render(request, "500.html", context, status=500) - - -@login_required -def map_client_proxy_view(request: HttpRequest): - """ Provides proxy functionality for NETGIS map client. - - Used for fetching content of a provided url - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - url = request.META.get("QUERY_STRING") - response = requests.get(url) - body = json.loads(response.content) - if response.status_code != 200: - return JsonResponse({ - "status_code": response.status_code, - "content": body, - }) - return JsonResponse(body) diff --git a/konova/views/__init__.py b/konova/views/__init__.py new file mode 100644 index 00000000..3a996e1f --- /dev/null +++ b/konova/views/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" diff --git a/konova/views/error.py b/konova/views/error.py new file mode 100644 index 00000000..6a85948a --- /dev/null +++ b/konova/views/error.py @@ -0,0 +1,38 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 16.11.20 + +""" +from django.http import HttpRequest +from django.shortcuts import render + +from konova.contexts import BaseContext + + +def get_404_view(request: HttpRequest, exception=None): + """ Returns a 404 handling view + + Args: + request (): + exception (): + + Returns: + + """ + context = BaseContext.context + return render(request, "404.html", context, status=404) + + +def get_500_view(request: HttpRequest): + """ Returns a 404 handling view + + Args: + request (): + + Returns: + + """ + context = BaseContext.context + return render(request, "500.html", context, status=500) diff --git a/konova/views/geometry.py b/konova/views/geometry.py new file mode 100644 index 00000000..e2e8737f --- /dev/null +++ b/konova/views/geometry.py @@ -0,0 +1,108 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.http import HttpResponse, HttpRequest +from django.shortcuts import get_object_or_404 +from django.template.loader import render_to_string + +from konova.models import Geometry, Municipal + + +def get_geom_parcels(request: HttpRequest, id: str): + """ Getter for HTMX + + Returns all parcels of the requested geometry rendered into a simple HTML table + + Args: + request (HttpRequest): The incoming request + id (str): The geometry's id + + Returns: + A rendered piece of HTML + """ + # HTTP code 286 states that the HTMX should stop polling for updates + # https://htmx.org/docs/#polling + status_code = 286 + template = "konova/includes/parcels/parcel_table_frame.html" + geom = get_object_or_404(Geometry, id=id) + parcels = geom.get_underlying_parcels() + geos_geom = geom.geom + + parcels_are_currently_calculated = geos_geom is not None and geos_geom.area > 0 and len(parcels) == 0 + parcels_available = len(parcels) > 0 + no_geometry_given = geos_geom is None + + if parcels_are_currently_calculated: + # Parcels are being calculated right now. Change the status code, so polling stays active for fetching + # resutls after the calculation + status_code = 200 + + if parcels_available or no_geometry_given: + parcels = parcels.order_by("-municipal", "flr", "flrstck_zhlr", "flrstck_nnr") + municipals = parcels.order_by("municipal").distinct("municipal").values("municipal__id") + municipals = Municipal.objects.filter(id__in=municipals) + + rpp = 100 + num_all_parcels = parcels.count() + parcels = parcels[:rpp] + next_page = 1 + if len(parcels) < rpp: + next_page = None + + context = { + "num_parcels": num_all_parcels, + "parcels": parcels, + "municipals": municipals, + "geom_id": str(id), + "next_page": next_page, + } + html = render_to_string(template, context, request) + return HttpResponse(html, status=status_code) + else: + return HttpResponse(None, status=404) + + +def get_geom_parcels_content(request: HttpRequest, id: str, page: int): + """ Getter for infinite scroll of HTMX + + Returns parcels of a specific page/slice of the found parcel set. + Implementation of infinite scroll htmx example: https://htmx.org/examples/infinite-scroll/ + + Args: + request (HttpRequest): The incoming request + id (str): The geometry's id + page (int): The requested page number + + Returns: + A rendered piece of HTML + """ + if page < 0: + raise AssertionError("Parcel page can not be negative") + + # HTTP code 286 states that the HTMX should stop polling for updates + # https://htmx.org/docs/#polling + status_code = 286 + template = "konova/includes/parcels/parcel_table_content.html" + geom = get_object_or_404(Geometry, id=id) + parcels = geom.get_underlying_parcels() + + parcels = parcels.order_by("-municipal", "flr", "flrstck_zhlr", "flrstck_nnr") + rpp = 100 + from_p = rpp * (page-1) + to_p = rpp * (page) + next_page = page + 1 + parcels = parcels[from_p:to_p] + if len(parcels) < rpp: + next_page = None + + context = { + "parcels": parcels, + "geom_id": str(id), + "next_page": next_page, + } + html = render_to_string(template, context, request) + return HttpResponse(html, status=status_code) diff --git a/konova/views/home.py b/konova/views/home.py new file mode 100644 index 00000000..3118956c --- /dev/null +++ b/konova/views/home.py @@ -0,0 +1,82 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import render +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from compensation.models import EcoAccount, Compensation +from intervention.models import Intervention +from konova.contexts import BaseContext +from konova.decorators import any_group_check +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from news.models import ServerMessage + + +@login_required +@any_group_check +def home_view(request: HttpRequest): + """ + Renders the landing page + + Args: + request (HttpRequest): The used request object + + Returns: + A redirect + """ + template = "konova/home.html" + now = timezone.now() + user = request.user + + # Fetch the four newest active and published ServerMessages + msgs = ServerMessage.objects.filter( + is_active=True, + publish_on__lte=now, + unpublish_on__gte=now, + ).order_by( + "-publish_on" + )[:3] + + # First fetch all valid objects (undeleted, only newest versions) + interventions = Intervention.objects.filter( + deleted=None, + ) + # Then fetch only user related ones + user_interventions = interventions.filter( + users__in=[user] + ) + + # Repeat for other objects + comps = Compensation.objects.filter( + deleted=None, + ) + user_comps = comps.filter( + intervention__users__in=[user] + ) + eco_accs = EcoAccount.objects.filter( + deleted=None, + ) + user_ecco_accs = eco_accs.filter( + users__in=[user] + ) + + additional_context = { + "msgs": msgs, + "total_intervention_count": interventions.count(), + "user_intervention_count": user_interventions.count(), + "total_compensation_count": comps.count(), + "user_compensation_count": user_comps.count(), + "total_eco_count": eco_accs.count(), + "user_eco_count": user_ecco_accs.count(), + TAB_TITLE_IDENTIFIER: _("Home"), + } + context = BaseContext(request, additional_context).context + return render(request, template, context) + diff --git a/konova/views/log.py b/konova/views/log.py new file mode 100644 index 00000000..b6d44c35 --- /dev/null +++ b/konova/views/log.py @@ -0,0 +1,41 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.shortcuts import get_object_or_404, render +from django.views import View +from django.utils.translation import gettext_lazy as _ + +from konova.contexts import BaseContext + + +class AbstractLogView(View): + model = None + + class Meta: + abstract = True + + def get(self, request, id: str): + """ Renders a log view using modal + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + + Returns: + + """ + intervention = get_object_or_404(self.model, id=id) + template = "modal/modal_generic.html" + body_template = "log.html" + + context = { + "modal_body_template": body_template, + "log": intervention.log.all(), + "modal_title": _("Log"), + } + context = BaseContext(request, context).context + return render(request, template, context) diff --git a/konova/views/logout.py b/konova/views/logout.py new file mode 100644 index 00000000..943673d0 --- /dev/null +++ b/konova/views/logout.py @@ -0,0 +1,26 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib.auth import logout +from django.http import HttpRequest +from django.shortcuts import redirect + +from konova.sub_settings.sso_settings import SSO_SERVER_BASE + + +def logout_view(request: HttpRequest): + """ + Logout route for ending the session manually. + + Args: + request (HttpRequest): The used request object + + Returns: + A redirect + """ + logout(request) + return redirect(SSO_SERVER_BASE) diff --git a/konova/views/map_proxy.py b/konova/views/map_proxy.py new file mode 100644 index 00000000..13920fd3 --- /dev/null +++ b/konova/views/map_proxy.py @@ -0,0 +1,35 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +import json + +import requests +from django.contrib.auth.decorators import login_required +from django.http import JsonResponse, HttpRequest + + +@login_required +def map_client_proxy_view(request: HttpRequest): + """ Provides proxy functionality for NETGIS map client. + + Used for fetching content of a provided url + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + url = request.META.get("QUERY_STRING") + response = requests.get(url) + body = json.loads(response.content) + if response.status_code != 200: + return JsonResponse({ + "status_code": response.status_code, + "content": body, + }) + return JsonResponse(body) From ef3507f0587dbf813c5388c51571aaf77649188f Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 19 Aug 2022 10:47:59 +0200 Subject: [PATCH 23/48] Resubmission class view * adds AbstractResubmissionView to konova app * implemented for all major data types * replaces function based views --- compensation/urls/compensation.py | 4 +- compensation/urls/eco_account.py | 4 +- .../views/compensation/resubmission.py | 36 ++++-------- .../views/eco_account/resubmission.py | 36 ++++-------- ema/urls.py | 4 +- ema/views/resubmission.py | 36 ++++-------- intervention/urls.py | 4 +- intervention/views/resubmission.py | 36 ++++-------- konova/views/resubmission.py | 55 +++++++++++++++++++ 9 files changed, 107 insertions(+), 108 deletions(-) create mode 100644 konova/views/resubmission.py diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index 5b27906e..f2ff5793 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -9,7 +9,7 @@ from django.urls import path from compensation.views.compensation.document import edit_document_view, new_document_view, remove_document_view, \ get_document_view -from compensation.views.compensation.resubmission import create_resubmission_view +from compensation.views.compensation.resubmission import CompensationResubmissionView from compensation.views.compensation.report import report_view from compensation.views.compensation.deadline import deadline_new_view, deadline_edit_view, deadline_remove_view from compensation.views.compensation.action import action_edit_view, action_new_view, action_remove_view @@ -41,7 +41,7 @@ urlpatterns = [ path('/deadline//edit', deadline_edit_view, name='deadline-edit'), path('/deadline//remove', deadline_remove_view, name='deadline-remove'), path('/report', report_view, name='report'), - path('/resub', create_resubmission_view, name='resubmission-create'), + path('/resub', CompensationResubmissionView.as_view(), name='resubmission-create'), # Documents path('/document/new/', new_document_view, name='new-doc'), diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index e15dd58f..8f9c3b19 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -13,7 +13,7 @@ from compensation.views.eco_account.eco_account import index_view, new_view, new from compensation.views.eco_account.log import EcoAccountLogView from compensation.views.eco_account.record import record_view from compensation.views.eco_account.report import report_view -from compensation.views.eco_account.resubmission import create_resubmission_view +from compensation.views.eco_account.resubmission import EcoAccountResubmissionView from compensation.views.eco_account.state import state_new_view, state_remove_view, state_edit_view from compensation.views.eco_account.action import action_edit_view, action_new_view, action_remove_view from compensation.views.eco_account.deadline import deadline_new_view, deadline_edit_view, deadline_remove_view @@ -33,7 +33,7 @@ urlpatterns = [ path('/report', report_view, name='report'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), - path('/resub', create_resubmission_view, name='resubmission-create'), + path('/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'), path('/state/new', state_new_view, name='new-state'), path('/state//edit', state_edit_view, name='state-edit'), diff --git a/compensation/views/compensation/resubmission.py b/compensation/views/compensation/resubmission.py index d90a91ce..dfcbad20 100644 --- a/compensation/views/compensation/resubmission.py +++ b/compensation/views/compensation/resubmission.py @@ -6,34 +6,20 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from compensation.models import Compensation from konova.decorators import shared_access_required, default_group_required -from konova.forms.modals import ResubmissionModalForm +from konova.views.resubmission import AbstractResubmissionView -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def create_resubmission_view(request: HttpRequest, id: str): - """ Renders resubmission form for a compensation +class CompensationResubmissionView(AbstractResubmissionView): + model = Compensation + redirect_url_base = "compensation:detail" + form_action_url_base = "compensation:resubmission-create" - Args: - request (HttpRequest): The incoming request - id (str): Compensation's id - - Returns: - - """ - com = get_object_or_404(Compensation, id=id) - form = ResubmissionModalForm(request.POST or None, instance=com, request=request) - form.action_url = reverse("compensation:resubmission-create", args=(id,)) - return form.process_request( - request, - msg_success=_("Resubmission set"), - redirect_url=reverse("compensation:detail", args=(id,)) - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/compensation/views/eco_account/resubmission.py b/compensation/views/eco_account/resubmission.py index 43c52224..afe8c713 100644 --- a/compensation/views/eco_account/resubmission.py +++ b/compensation/views/eco_account/resubmission.py @@ -6,34 +6,20 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from compensation.models import EcoAccount from konova.decorators import shared_access_required, default_group_required -from konova.forms.modals import ResubmissionModalForm +from konova.views.resubmission import AbstractResubmissionView -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def create_resubmission_view(request: HttpRequest, id: str): - """ Renders resubmission form for an eco account +class EcoAccountResubmissionView(AbstractResubmissionView): + model = EcoAccount + redirect_url_base = "compensation:acc:detail" + form_action_url_base = "compensation:acc:resubmission-create" - Args: - request (HttpRequest): The incoming request - id (str): EcoAccount's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - form = ResubmissionModalForm(request.POST or None, instance=acc, request=request) - form.action_url = reverse("compensation:acc:resubmission-create", args=(id,)) - return form.process_request( - request, - msg_success=_("Resubmission set"), - redirect_url=reverse("compensation:acc:detail", args=(id,)) - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/ema/urls.py b/ema/urls.py index d4972557..726ec653 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -14,7 +14,7 @@ from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_v from ema.views.log import EmaLogView from ema.views.record import record_view from ema.views.report import report_view -from ema.views.resubmission import create_resubmission_view +from ema.views.resubmission import EmaResubmissionView from ema.views.share import share_view, create_share_view from ema.views.state import state_new_view, state_remove_view, state_edit_view @@ -29,7 +29,7 @@ urlpatterns = [ path('/remove', remove_view, name='remove'), path('/record', record_view, name='record'), path('/report', report_view, name='report'), - path('/resub', create_resubmission_view, name='resubmission-create'), + path('/resub', EmaResubmissionView.as_view(), name='resubmission-create'), path('/state/new', state_new_view, name='new-state'), path('/state//remove', state_remove_view, name='state-remove'), diff --git a/ema/views/resubmission.py b/ema/views/resubmission.py index 838cab3a..384c3cde 100644 --- a/ema/views/resubmission.py +++ b/ema/views/resubmission.py @@ -6,34 +6,20 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from ema.models import Ema from konova.decorators import shared_access_required, conservation_office_group_required -from konova.forms.modals import ResubmissionModalForm +from konova.views.resubmission import AbstractResubmissionView -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def create_resubmission_view(request: HttpRequest, id: str): - """ Renders resubmission form for an EMA +class EmaResubmissionView(AbstractResubmissionView): + model = Ema + redirect_url_base = "ema:detail" + form_action_url_base = "ema:resubmission-create" - Args: - request (HttpRequest): The incoming request - id (str): EMA's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = ResubmissionModalForm(request.POST or None, instance=ema, request=request) - form.action_url = reverse("ema:resubmission-create", args=(id,)) - return form.process_request( - request, - msg_success=_("Resubmission set"), - redirect_url=reverse("ema:detail", args=(id,)) - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/intervention/urls.py b/intervention/urls.py index ed9dbaae..31148d33 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -16,7 +16,7 @@ from intervention.views.intervention import index_view, new_view, new_id_view, d from intervention.views.log import InterventionLogView from intervention.views.record import record_view from intervention.views.report import report_view -from intervention.views.resubmission import create_resubmission_view +from intervention.views.resubmission import InterventionResubmissionView from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \ get_revocation_view from intervention.views.share import share_view, create_share_view @@ -35,7 +35,7 @@ urlpatterns = [ path('/check', check_view, name='check'), path('/record', record_view, name='record'), path('/report', report_view, name='report'), - path('/resub', create_resubmission_view, name='resubmission-create'), + path('/resub', InterventionResubmissionView.as_view(), name='resubmission-create'), # Compensations path('/compensation//remove', remove_compensation_view, name='remove-compensation'), diff --git a/intervention/views/resubmission.py b/intervention/views/resubmission.py index 34a618c2..452a3e38 100644 --- a/intervention/views/resubmission.py +++ b/intervention/views/resubmission.py @@ -6,34 +6,20 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from intervention.models import Intervention from konova.decorators import default_group_required, shared_access_required -from konova.forms.modals import ResubmissionModalForm +from konova.views.resubmission import AbstractResubmissionView -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def create_resubmission_view(request: HttpRequest, id: str): - """ Renders resubmission form for an intervention +class InterventionResubmissionView(AbstractResubmissionView): + model = Intervention + redirect_url_base = "intervention:detail" + form_action_url_base = "intervention:resubmission-create" - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - form = ResubmissionModalForm(request.POST or None, instance=intervention, request=request) - form.action_url = reverse("intervention:resubmission-create", args=(id,)) - return form.process_request( - request, - msg_success=_("Resubmission set"), - redirect_url=reverse("intervention:detail", args=(id,)) - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/konova/views/resubmission.py b/konova/views/resubmission.py new file mode 100644 index 00000000..92ad056e --- /dev/null +++ b/konova/views/resubmission.py @@ -0,0 +1,55 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.views import View +from django.utils.translation import gettext_lazy as _ + +from konova.forms.modals import ResubmissionModalForm + + +class AbstractResubmissionView(View): + model = None + form_action_url_base = None + redirect_url_base = None + + class Meta: + abstract = True + + def get(self, request, id: str): + """ Renders resubmission form for an intervention + + Args: + request (HttpRequest): The incoming request + id (str): Intervention's id + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + form = ResubmissionModalForm(request.POST or None, instance=obj, request=request) + form.action_url = reverse(self.form_action_url_base, args=(id,)) + return form.process_request( + request, + msg_success=_("Resubmission set"), + redirect_url=reverse(self.redirect_url_base, args=(id,)) + ) + + def post(self, request, id: str): + """ + + BaseModalForm provides the method process_request() which handles GET as well as POST requests. It was written + for easier handling of function based views. To support process_request() on class based views, the post() + call needs to be treated the same way as the get() call. + + Args: + request (HttpRequest): The incoming request + id (str): Intervention's id + + """ + return self.get(request, id) From 7b35591f5d69612996af4c8b0fc31f607b7c0890 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 19 Aug 2022 11:01:33 +0200 Subject: [PATCH 24/48] Record class view * adds AbstractRecordView to konova/views/record.py * implements for all major data types --- compensation/urls/eco_account.py | 4 +- compensation/views/eco_account/record.py | 33 +++++----------- ema/urls.py | 4 +- ema/views/record.py | 32 +++++---------- intervention/urls.py | 4 +- intervention/views/record.py | 34 +++++----------- konova/views/log.py | 2 +- konova/views/record.py | 50 ++++++++++++++++++++++++ konova/views/resubmission.py | 4 +- 9 files changed, 86 insertions(+), 81 deletions(-) create mode 100644 konova/views/record.py diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index 8f9c3b19..862d5f4a 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -11,7 +11,7 @@ from compensation.autocomplete.eco_account import EcoAccountAutocomplete from compensation.views.eco_account.eco_account import index_view, new_view, new_id_view, edit_view, remove_view, \ detail_view from compensation.views.eco_account.log import EcoAccountLogView -from compensation.views.eco_account.record import record_view +from compensation.views.eco_account.record import EcoAccountRecordView from compensation.views.eco_account.report import report_view from compensation.views.eco_account.resubmission import EcoAccountResubmissionView from compensation.views.eco_account.state import state_new_view, state_remove_view, state_edit_view @@ -29,7 +29,7 @@ urlpatterns = [ path('new/id', new_id_view, name='new-id'), path('', detail_view, name='detail'), path('/log', EcoAccountLogView.as_view(), name='log'), - path('/record', record_view, name='record'), + path('/record', EcoAccountRecordView.as_view(), name='record'), path('/report', report_view, name='report'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), diff --git a/compensation/views/eco_account/record.py b/compensation/views/eco_account/record.py index c1fe1384..18ccf237 100644 --- a/compensation/views/eco_account/record.py +++ b/compensation/views/eco_account/record.py @@ -6,33 +6,18 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from compensation.models import EcoAccount from konova.decorators import shared_access_required, conservation_office_group_required -from konova.forms.modals import RecordModalForm +from konova.views.record import AbstractRecordView -@login_required -@conservation_office_group_required -@shared_access_required(EcoAccount, "id") -def record_view(request: HttpRequest, id:str): - """ Renders a modal form for recording an eco account +class EcoAccountRecordView(AbstractRecordView): + model = EcoAccount - Args: - request (HttpRequest): The incoming request - id (str): The account's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - form = RecordModalForm(request.POST or None, instance=acc, request=request) - msg_succ = _("{} unrecorded") if acc.recorded else _("{} recorded") - msg_succ = msg_succ.format(acc.identifier) - return form.process_request( - request, - msg_succ - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/ema/urls.py b/ema/urls.py index 726ec653..f68145ba 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -12,7 +12,7 @@ from ema.views.deadline import deadline_new_view, deadline_edit_view, deadline_r from ema.views.document import document_new_view, get_document_view, remove_document_view, edit_document_view from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_view, remove_view from ema.views.log import EmaLogView -from ema.views.record import record_view +from ema.views.record import EmaRecordView from ema.views.report import report_view from ema.views.resubmission import EmaResubmissionView from ema.views.share import share_view, create_share_view @@ -27,7 +27,7 @@ urlpatterns = [ path('/log', EmaLogView.as_view(), name='log'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), - path('/record', record_view, name='record'), + path('/record', EmaRecordView.as_view(), name='record'), path('/report', report_view, name='report'), path('/resub', EmaResubmissionView.as_view(), name='resubmission-create'), diff --git a/ema/views/record.py b/ema/views/record.py index 4ae40d2f..7dfc946b 100644 --- a/ema/views/record.py +++ b/ema/views/record.py @@ -6,32 +6,18 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from ema.models import Ema from konova.decorators import shared_access_required, conservation_office_group_required -from konova.forms.modals import RecordModalForm +from konova.views.record import AbstractRecordView -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def record_view(request: HttpRequest, id: str): - """ Renders a modal view for recording the EMA +class EmaRecordView(AbstractRecordView): + model = Ema - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - msg_succ = _("{} unrecorded") if ema.recorded else _("{} recorded") - form = RecordModalForm(request.POST or None, instance=ema, request=request) - return form.process_request( - request=request, - msg_success=msg_succ.format("EMA"), - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/intervention/urls.py b/intervention/urls.py index 31148d33..284a3440 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -14,7 +14,7 @@ from intervention.views.deduction import new_deduction_view, edit_deduction_view from intervention.views.document import new_document_view, get_document_view, remove_document_view, edit_document_view from intervention.views.intervention import index_view, new_view, new_id_view, detail_view, edit_view, remove_view from intervention.views.log import InterventionLogView -from intervention.views.record import record_view +from intervention.views.record import InterventionRecordView from intervention.views.report import report_view from intervention.views.resubmission import InterventionResubmissionView from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \ @@ -33,7 +33,7 @@ urlpatterns = [ path('/share/', share_view, name='share'), path('/share', create_share_view, name='share-create'), path('/check', check_view, name='check'), - path('/record', record_view, name='record'), + path('/record', InterventionRecordView.as_view(), name='record'), path('/report', report_view, name='report'), path('/resub', InterventionResubmissionView.as_view(), name='resubmission-create'), diff --git a/intervention/views/record.py b/intervention/views/record.py index c5709b22..a845fdfd 100644 --- a/intervention/views/record.py +++ b/intervention/views/record.py @@ -6,34 +6,18 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from intervention.models import Intervention from konova.decorators import conservation_office_group_required, shared_access_required -from konova.forms.modals import RecordModalForm +from konova.views.record import AbstractRecordView -@login_required -@conservation_office_group_required -@shared_access_required(Intervention, "id") -def record_view(request: HttpRequest, id: str): - """ Renders a modal form for recording an intervention +class InterventionRecordView(AbstractRecordView): + model = Intervention - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - form = RecordModalForm(request.POST or None, instance=intervention, request=request) - msg_succ = _("{} unrecorded") if intervention.recorded else _("{} recorded") - msg_succ = msg_succ.format(intervention.identifier) - return form.process_request( - request, - msg_succ, - msg_error=_("There are errors on this intervention:") - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/konova/views/log.py b/konova/views/log.py index b6d44c35..7cc9d56a 100644 --- a/konova/views/log.py +++ b/konova/views/log.py @@ -23,7 +23,7 @@ class AbstractLogView(View): Args: request (HttpRequest): The incoming request - id (str): The compensation's id + id (str): The object's id Returns: diff --git a/konova/views/record.py b/konova/views/record.py new file mode 100644 index 00000000..b80d1b0f --- /dev/null +++ b/konova/views/record.py @@ -0,0 +1,50 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.shortcuts import get_object_or_404 +from django.views import View +from django.utils.translation import gettext_lazy as _ + +from konova.forms.modals import RecordModalForm + + +class AbstractRecordView(View): + model = None + + def get(self, request, id: str): + """ Renders a modal form for recording an object + + Args: + request (HttpRequest): The incoming request + id (str): The object's id + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + form = RecordModalForm(request.POST or None, instance=obj, request=request) + msg_succ = _("{} unrecorded") if obj.recorded else _("{} recorded") + msg_succ = msg_succ.format(obj.identifier) + return form.process_request( + request, + msg_succ, + msg_error=_("Errors found:") + ) + + def post(self, request, id: str): + """ + + BaseModalForm provides the method process_request() which handles GET as well as POST requests. It was written + for easier handling of function based views. To support process_request() on class based views, the post() + call needs to be treated the same way as the get() call. + + Args: + request (HttpRequest): The incoming request + id (str): Intervention's id + + """ + return self.get(request, id) diff --git a/konova/views/resubmission.py b/konova/views/resubmission.py index 92ad056e..634949de 100644 --- a/konova/views/resubmission.py +++ b/konova/views/resubmission.py @@ -22,11 +22,11 @@ class AbstractResubmissionView(View): abstract = True def get(self, request, id: str): - """ Renders resubmission form for an intervention + """ Renders resubmission form for an object Args: request (HttpRequest): The incoming request - id (str): Intervention's id + id (str): Object's id Returns: From a73046aa02ef7eeb2675fd7a8a44e0c76cb2142c Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 22 Aug 2022 07:52:22 +0200 Subject: [PATCH 25/48] Document views * replaces function based views for creating, editing, removing and fetching documents with class based views * implemented for all major data types --- compensation/urls/compensation.py | 12 +- compensation/urls/eco_account.py | 12 +- compensation/views/compensation/document.py | 124 ++++++------------ compensation/views/eco_account/document.py | 119 ++++++----------- ema/urls.py | 10 +- ema/views/document.py | 124 ++++++------------ intervention/urls.py | 11 +- intervention/views/document.py | 125 ++++++------------ konova/views/document.py | 138 ++++++++++++++++++++ 9 files changed, 313 insertions(+), 362 deletions(-) create mode 100644 konova/views/document.py diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index f2ff5793..ea60bafa 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -7,8 +7,8 @@ Created on: 24.08.21 """ from django.urls import path -from compensation.views.compensation.document import edit_document_view, new_document_view, remove_document_view, \ - get_document_view +from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \ + GetCompensationDocumentView, RemoveCompensationDocumentView from compensation.views.compensation.resubmission import CompensationResubmissionView from compensation.views.compensation.report import report_view from compensation.views.compensation.deadline import deadline_new_view, deadline_edit_view, deadline_remove_view @@ -44,9 +44,9 @@ urlpatterns = [ path('/resub', CompensationResubmissionView.as_view(), name='resubmission-create'), # Documents - path('/document/new/', new_document_view, name='new-doc'), - path('/document/', get_document_view, name='get-doc'), - path('/document//remove/', remove_document_view, name='remove-doc'), - path('/document//edit/', edit_document_view, name='edit-doc'), + path('/document/new/', NewCompensationDocumentView.as_view(), name='new-doc'), + path('/document/', GetCompensationDocumentView.as_view(), name='get-doc'), + path('/document//remove/', RemoveCompensationDocumentView.as_view(), name='remove-doc'), + path('/document//edit/', EditCompensationDocumentView.as_view(), name='edit-doc'), ] \ No newline at end of file diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index 862d5f4a..252d8e67 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -18,8 +18,8 @@ from compensation.views.eco_account.state import state_new_view, state_remove_vi from compensation.views.eco_account.action import action_edit_view, action_new_view, action_remove_view from compensation.views.eco_account.deadline import deadline_new_view, deadline_edit_view, deadline_remove_view from compensation.views.eco_account.share import share_view, create_share_view -from compensation.views.eco_account.document import get_document_view, new_document_view, remove_document_view, \ - edit_document_view +from compensation.views.eco_account.document import GetEcoAccountDocumentView, NewEcoAccountDocumentView, \ + EditEcoAccountDocumentView, RemoveEcoAccountDocumentView from compensation.views.eco_account.deduction import deduction_edit_view, deduction_remove_view, new_deduction_view app_name = "acc" @@ -51,10 +51,10 @@ urlpatterns = [ path('/share', create_share_view, name='share-create'), # Documents - path('/document/new/', new_document_view, name='new-doc'), - path('/document/', get_document_view, name='get-doc'), - path('/document//edit', edit_document_view, name='edit-doc'), - path('/document//remove/', remove_document_view, name='remove-doc'), + path('/document/new/', NewEcoAccountDocumentView.as_view(), name='new-doc'), + path('/document/', GetEcoAccountDocumentView.as_view(), name='get-doc'), + path('/document//edit', EditEcoAccountDocumentView.as_view(), name='edit-doc'), + path('/document//remove/', RemoveEcoAccountDocumentView.as_view(), name='remove-doc'), # Eco-account deductions path('/deduction//remove', deduction_remove_view, name='remove-deduction'), diff --git a/compensation/views/compensation/document.py b/compensation/views/compensation/document.py index 8ca99edd..cb31739e 100644 --- a/compensation/views/compensation/document.py +++ b/compensation/views/compensation/document.py @@ -6,106 +6,58 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse +from django.utils.decorators import method_decorator from compensation.forms.modals.document import NewCompensationDocumentModalForm from compensation.models import Compensation, CompensationDocument from konova.decorators import shared_access_required, default_group_required from konova.forms.modals import EditDocumentModalForm -from konova.utils.documents import remove_document, get_document -from konova.utils.message_templates import DOCUMENT_EDITED, DOCUMENT_ADDED +from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ + AbstractEditDocumentView -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def new_document_view(request: HttpRequest, id: str): - """ Renders a form for uploading new documents +class NewCompensationDocumentView(AbstractNewDocumentView): + model = Compensation + form = NewCompensationDocumentModalForm + redirect_url = "compensation:detail" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id to which the new document will be related - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - form = NewCompensationDocumentModalForm(request.POST or None, request.FILES or None, instance=comp, request=request) - return form.process_request( - request, - msg_success=DOCUMENT_ADDED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def get_document_view(request: HttpRequest, id: str, doc_id: str): - """ Returns the document as downloadable file +class GetCompensationDocumentView(AbstractGetDocumentView): + model = Compensation + document_model = CompensationDocument - Wraps the generic document fetcher function from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The compensation id - doc_id (str): The document id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - doc = get_object_or_404(CompensationDocument, id=doc_id) - return get_document(doc) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def remove_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system +class RemoveCompensationDocumentView(AbstractRemoveDocumentView): + model = Compensation + document_model = CompensationDocument - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The compensation id - doc_id (str): The document id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - doc = get_object_or_404(CompensationDocument, id=doc_id) - return remove_document( - request, - doc - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def edit_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The compensation id - doc_id (str): The document id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - doc = get_object_or_404(CompensationDocument, id=doc_id) - form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=comp, document=doc, request=request) - return form.process_request( - request, - DOCUMENT_EDITED, - reverse("compensation:detail", args=(id,)) + "#related_data" - ) +class EditCompensationDocumentView(AbstractEditDocumentView): + model = Compensation + document_model = CompensationDocument + form = EditDocumentModalForm + redirect_url = "compensation:detail" + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/compensation/views/eco_account/document.py b/compensation/views/eco_account/document.py index 53e0a200..40b40a04 100644 --- a/compensation/views/eco_account/document.py +++ b/compensation/views/eco_account/document.py @@ -9,6 +9,7 @@ from django.contrib.auth.decorators import login_required from django.http import HttpRequest from django.shortcuts import get_object_or_404 from django.urls import reverse +from django.utils.decorators import method_decorator from compensation.forms.modals.document import NewEcoAccountDocumentModalForm from compensation.models import EcoAccount, EcoAccountDocument @@ -16,96 +17,52 @@ from konova.decorators import shared_access_required, default_group_required from konova.forms.modals import EditDocumentModalForm from konova.utils.documents import remove_document, get_document from konova.utils.message_templates import DOCUMENT_EDITED, DOCUMENT_ADDED +from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ + AbstractEditDocumentView -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def new_document_view(request: HttpRequest, id: str): - """ Renders a form for uploading new documents +class NewEcoAccountDocumentView(AbstractNewDocumentView): + model = EcoAccount + form = NewEcoAccountDocumentModalForm + redirect_url = "compensation:acc:detail" - Args: - request (HttpRequest): The incoming request - id (str): The account's id to which the new document will be related - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - form = NewEcoAccountDocumentModalForm(request.POST or None, request.FILES or None, instance=acc, request=request) - return form.process_request( - request, - msg_success=DOCUMENT_ADDED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data", - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def get_document_view(request: HttpRequest, id:str, doc_id: str): - """ Returns the document as downloadable file +class GetEcoAccountDocumentView(AbstractGetDocumentView): + model = EcoAccount + document_model = EcoAccountDocument - Wraps the generic document fetcher function from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The account id - doc_id (str): The document id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - doc = get_object_or_404(EcoAccountDocument, id=doc_id) - return get_document(doc) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def edit_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system +class RemoveEcoAccountDocumentView(AbstractRemoveDocumentView): + model = EcoAccount + document_model = EcoAccountDocument - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The account id - doc_id (str): The document id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - doc = get_object_or_404(EcoAccountDocument, id=doc_id) - form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=acc, document=doc, request=request) - return form.process_request( - request, - DOCUMENT_EDITED, - reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def remove_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The account id - doc_id (str): The document id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - doc = get_object_or_404(EcoAccountDocument, id=doc_id) - return remove_document( - request, - doc - ) +class EditEcoAccountDocumentView(AbstractEditDocumentView): + model = EcoAccount + document_model = EcoAccountDocument + form = EditDocumentModalForm + redirect_url = "compensation:acc:detail" + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/ema/urls.py b/ema/urls.py index f68145ba..770e24c8 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -9,7 +9,7 @@ from django.urls import path from ema.views.action import action_new_view, action_edit_view, action_remove_view from ema.views.deadline import deadline_new_view, deadline_edit_view, deadline_remove_view -from ema.views.document import document_new_view, get_document_view, remove_document_view, edit_document_view +from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_view, remove_view from ema.views.log import EmaLogView from ema.views.record import EmaRecordView @@ -47,9 +47,9 @@ urlpatterns = [ path('/share', create_share_view, name='share-create'), # Documents - path('/document/new/', document_new_view, name='new-doc'), - path('/document/', get_document_view, name='get-doc'), - path('/document//edit/', edit_document_view, name='edit-doc'), - path('/document//remove/', remove_document_view, name='remove-doc'), + path('/document/new/', NewEmaDocumentView.as_view(), name='new-doc'), + path('/document/', GetEmaDocumentView.as_view(), name='get-doc'), + path('/document//edit/', EditEmaDocumentView.as_view(), name='edit-doc'), + path('/document//remove/', RemoveEmaDocumentView.as_view(), name='remove-doc'), ] \ No newline at end of file diff --git a/ema/views/document.py b/ema/views/document.py index faacb5bc..d394c0fe 100644 --- a/ema/views/document.py +++ b/ema/views/document.py @@ -6,106 +6,58 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse +from django.utils.decorators import method_decorator from ema.forms import NewEmaDocumentModalForm from ema.models import Ema, EmaDocument from konova.decorators import shared_access_required, conservation_office_group_required from konova.forms.modals import EditDocumentModalForm -from konova.utils.documents import get_document, remove_document -from konova.utils.message_templates import DOCUMENT_ADDED, DOCUMENT_EDITED +from konova.views.document import AbstractEditDocumentView, AbstractRemoveDocumentView, AbstractGetDocumentView, \ + AbstractNewDocumentView -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def document_new_view(request: HttpRequest, id: str): - """ Renders a form for uploading new documents +class NewEmaDocumentView(AbstractNewDocumentView): + model = Ema + form = NewEmaDocumentModalForm + redirect_url = "ema:detail" - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id to which the new document will be related - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = NewEmaDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, request=request) - return form.process_request( - request, - msg_success=DOCUMENT_ADDED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def get_document_view(request: HttpRequest, id: str, doc_id: str): - """ Returns the document as downloadable file +class GetEmaDocumentView(AbstractGetDocumentView): + model = Ema + document_model = EmaDocument - Wraps the generic document fetcher function from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The EMA id - doc_id (str): The document id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - doc = get_object_or_404(EmaDocument, id=doc_id) - return get_document(doc) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def edit_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system +class RemoveEmaDocumentView(AbstractRemoveDocumentView): + model = Ema + document_model = EmaDocument - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The EMA id - doc_id (str): The document id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - doc = get_object_or_404(EmaDocument, id=doc_id) - form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, document=doc, request=request) - return form.process_request( - request, - DOCUMENT_EDITED, - reverse("ema:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def remove_document_view(request: HttpRequest, id:str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The EMA id - doc_id (str): The document id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - doc = get_object_or_404(EmaDocument, id=doc_id) - return remove_document( - request, - doc - ) +class EditEmaDocumentView(AbstractEditDocumentView): + model = Ema + document_model = EmaDocument + form = EditDocumentModalForm + redirect_url = "ema:detail" + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/intervention/urls.py b/intervention/urls.py index 284a3440..20bd5c16 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -11,7 +11,8 @@ from intervention.autocomplete.intervention import InterventionAutocomplete from intervention.views.check import check_view from intervention.views.compensation import remove_compensation_view from intervention.views.deduction import new_deduction_view, edit_deduction_view, remove_deduction_view -from intervention.views.document import new_document_view, get_document_view, remove_document_view, edit_document_view +from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \ + RemoveInterventionDocumentView, EditInterventionDocumentView from intervention.views.intervention import index_view, new_view, new_id_view, detail_view, edit_view, remove_view from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView @@ -41,10 +42,10 @@ urlpatterns = [ path('/compensation//remove', remove_compensation_view, name='remove-compensation'), # Documents - path('/document/new/', new_document_view, name='new-doc'), - path('/document/', get_document_view, name='get-doc'), - path('/document//remove/', remove_document_view, name='remove-doc'), - path('/document//edit/', edit_document_view, name='edit-doc'), + path('/document/new/', NewInterventionDocumentView.as_view(), name='new-doc'), + path('/document/', GetInterventionDocumentView.as_view(), name='get-doc'), + path('/document//remove/', RemoveInterventionDocumentView.as_view(), name='remove-doc'), + path('/document//edit/', EditInterventionDocumentView.as_view(), name='edit-doc'), # Deductions path('/deduction/new', new_deduction_view, name='new-deduction'), diff --git a/intervention/views/document.py b/intervention/views/document.py index be43fbd5..80612781 100644 --- a/intervention/views/document.py +++ b/intervention/views/document.py @@ -6,107 +6,58 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from intervention.forms.modals.document import NewInterventionDocumentModalForm from intervention.models import Intervention, InterventionDocument from konova.decorators import default_group_required, shared_access_required from konova.forms.modals import EditDocumentModalForm -from konova.utils.documents import get_document, remove_document -from konova.utils.message_templates import DOCUMENT_ADDED, DOCUMENT_EDITED +from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ + AbstractEditDocumentView -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def new_document_view(request: HttpRequest, id: str): - """ Renders a form for uploading new documents +class NewInterventionDocumentView(AbstractNewDocumentView): + model = Intervention + form = NewInterventionDocumentModalForm + redirect_url = "intervention:detail" - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id to which the new document will be related - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - form = NewInterventionDocumentModalForm(request.POST or None, request.FILES or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=DOCUMENT_ADDED, - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def get_document_view(request: HttpRequest, id: str, doc_id: str): - """ Returns the document as downloadable file +class GetInterventionDocumentView(AbstractGetDocumentView): + model = Intervention + document_model = InterventionDocument - Wraps the generic document fetcher function from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The intervention id - doc_id (str): The document id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - doc = get_object_or_404(InterventionDocument, id=doc_id) - return get_document(doc) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def remove_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system +class RemoveInterventionDocumentView(AbstractRemoveDocumentView): + model = Intervention + document_model = InterventionDocument - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The intervention id - doc_id (str): The document id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - doc = get_object_or_404(InterventionDocument, id=doc_id) - return remove_document( - request, - doc - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def edit_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The intervention id - doc_id (str): The document id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - doc = get_object_or_404(InterventionDocument, id=doc_id) - form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=intervention, document=doc, request=request) - return form.process_request( - request, - DOCUMENT_EDITED, - redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" - ) +class EditInterventionDocumentView(AbstractEditDocumentView): + model = Intervention + document_model = InterventionDocument + form = EditDocumentModalForm + redirect_url = "intervention:detail" + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/konova/views/document.py b/konova/views/document.py new file mode 100644 index 00000000..8dba6fd5 --- /dev/null +++ b/konova/views/document.py @@ -0,0 +1,138 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 22.08.22 + +""" +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.views import View + +from konova.utils.documents import get_document, remove_document +from konova.utils.message_templates import DOCUMENT_ADDED, DOCUMENT_EDITED + + +class AbstractNewDocumentView(View): + model = None + form = None + redirect_url = None + + class Meta: + abstract = True + + def get(self, request, id: str): + """ Renders a form for uploading new documents + + Args: + request (HttpRequest): The incoming request + id (str): The object's id to which the new document will be related + Returns: + + """ + intervention = get_object_or_404(self.model, id=id) + form = self.form(request.POST or None, request.FILES or None, instance=intervention, request=request) + return form.process_request( + request, + msg_success=DOCUMENT_ADDED, + redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + ) + + def post(self, request, id: str): + return self.get(request, id) + + +class AbstractGetDocumentView(View): + model = None + document_model = None + + class Meta: + abstract = True + + def get(self, request, id: str, doc_id: str): + """ Returns the document as downloadable file + + Wraps the generic document fetcher function from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The object id + doc_id (str): The document id + + Returns: + + """ + get_object_or_404(self.model, id=id) + doc = get_object_or_404(self.document_model, id=doc_id) + return get_document(doc) + + def post(self, request, id: str, doc_id: str): + return self.get(request, id, doc_id) + + +class AbstractRemoveDocumentView(View): + model = None + document_model = None + + class Meta: + abstract = True + + def get(self, request, id: str, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The intervention id + doc_id (str): The document id + + Returns: + + """ + get_object_or_404(self.model, id=id) + doc = get_object_or_404(self.document_model, id=doc_id) + return remove_document( + request, + doc + ) + + def post(self, request, id: str, doc_id: str): + return self.get(request, id, doc_id) + + +class AbstractEditDocumentView(View): + model = None + document_model = None + form = None + redirect_url = None + + class Meta: + abstract = True + + def get(self, request, id: str, doc_id: str): + """ GET handling for editing of existing document + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The intervention id + doc_id (str): The document id + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + doc = get_object_or_404(self.document_model, id=doc_id) + form = self.form(request.POST or None, request.FILES or None, instance=obj, document=doc, + request=request) + return form.process_request( + request, + DOCUMENT_EDITED, + redirect_url=reverse(self.redirect_url, args=(obj.id,)) + "#related_data" + ) + + def post(self, request, id: str, doc_id: str): + return self.get(request, id, doc_id) + From 9e6a9f4902a0eeaa17999a451d5fecf3728b38bb Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 22 Aug 2022 08:07:35 +0200 Subject: [PATCH 26/48] Deadline views * replaces function based views for deadlines with class based views --- compensation/urls/compensation.py | 9 +- compensation/urls/eco_account.py | 9 +- compensation/views/compensation/deadline.py | 95 +++++------------- compensation/views/eco_account/deadline.py | 95 +++++------------- konova/views/deadline.py | 106 ++++++++++++++++++++ 5 files changed, 168 insertions(+), 146 deletions(-) create mode 100644 konova/views/deadline.py diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index ea60bafa..dd089510 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -11,7 +11,8 @@ from compensation.views.compensation.document import EditCompensationDocumentVie GetCompensationDocumentView, RemoveCompensationDocumentView from compensation.views.compensation.resubmission import CompensationResubmissionView from compensation.views.compensation.report import report_view -from compensation.views.compensation.deadline import deadline_new_view, deadline_edit_view, deadline_remove_view +from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \ + RemoveCompensationDeadlineView from compensation.views.compensation.action import action_edit_view, action_new_view, action_remove_view from compensation.views.compensation.state import state_new_view, state_remove_view, state_edit_view from compensation.views.compensation.compensation import index_view, new_view, new_id_view, detail_view, edit_view, \ @@ -37,9 +38,9 @@ urlpatterns = [ path('/action//edit', action_edit_view, name='action-edit'), path('/action//remove', action_remove_view, name='action-remove'), - path('/deadline/new', deadline_new_view, name="new-deadline"), - path('/deadline//edit', deadline_edit_view, name='deadline-edit'), - path('/deadline//remove', deadline_remove_view, name='deadline-remove'), + path('/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"), + path('/deadline//edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'), + path('/deadline//remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'), path('/report', report_view, name='report'), path('/resub', CompensationResubmissionView.as_view(), name='resubmission-create'), diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index 252d8e67..90784fb4 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -16,7 +16,8 @@ from compensation.views.eco_account.report import report_view from compensation.views.eco_account.resubmission import EcoAccountResubmissionView from compensation.views.eco_account.state import state_new_view, state_remove_view, state_edit_view from compensation.views.eco_account.action import action_edit_view, action_new_view, action_remove_view -from compensation.views.eco_account.deadline import deadline_new_view, deadline_edit_view, deadline_remove_view +from compensation.views.eco_account.deadline import NewEcoAccountDeadlineView, EditEcoAccountDeadlineView, \ + RemoveEcoAccountDeadlineView from compensation.views.eco_account.share import share_view, create_share_view from compensation.views.eco_account.document import GetEcoAccountDocumentView, NewEcoAccountDocumentView, \ EditEcoAccountDocumentView, RemoveEcoAccountDocumentView @@ -43,9 +44,9 @@ urlpatterns = [ path('/action//edit', action_edit_view, name='action-edit'), path('/action//remove', action_remove_view, name='action-remove'), - path('/deadline/new', deadline_new_view, name="new-deadline"), - path('/deadline//edit', deadline_edit_view, name='deadline-edit'), - path('/deadline//remove', deadline_remove_view, name='deadline-remove'), + path('/deadline/new', NewEcoAccountDeadlineView.as_view(), name="new-deadline"), + path('/deadline//edit', EditEcoAccountDeadlineView.as_view(), name='deadline-edit'), + path('/deadline//remove', RemoveEcoAccountDeadlineView.as_view(), name='deadline-remove'), path('/share/', share_view, name='share'), path('/share', create_share_view, name='share-create'), diff --git a/compensation/views/compensation/deadline.py b/compensation/views/compensation/deadline.py index ee6a5120..7463b7ec 100644 --- a/compensation/views/compensation/deadline.py +++ b/compensation/views/compensation/deadline.py @@ -6,84 +6,41 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse +from django.utils.decorators import method_decorator -from compensation.forms.modals.deadline import EditDeadlineModalForm, NewDeadlineModalForm from compensation.models import Compensation from konova.decorators import shared_access_required, default_group_required -from konova.forms.modals import RemoveDeadlineModalForm -from konova.models import Deadline -from konova.utils.message_templates import DEADLINE_REMOVED, DEADLINE_EDITED, DEADLINE_ADDED +from konova.views.deadline import AbstractRemoveDeadlineView, AbstractEditDeadlineView, AbstractNewDeadlineView -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def deadline_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for a compensation +class NewCompensationDeadlineView(AbstractNewDeadlineView): + model = Compensation + redirect_url = "compensation:detail" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id to which the new state will be related - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - form = NewDeadlineModalForm(request.POST or None, instance=comp, request=request) - return form.process_request( - request, - msg_success=DEADLINE_ADDED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for editing deadlines from a compensation +class EditCompensationDeadlineView(AbstractEditDeadlineView): + model = Compensation + redirect_url = "compensation:detail" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = EditDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_EDITED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for removing deadlines from a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = RemoveDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_REMOVED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) +class RemoveCompensationDeadlineView(AbstractRemoveDeadlineView): + model = Compensation + redirect_url = "compensation:detail" + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/compensation/views/eco_account/deadline.py b/compensation/views/eco_account/deadline.py index 37e35202..15b4aa8c 100644 --- a/compensation/views/eco_account/deadline.py +++ b/compensation/views/eco_account/deadline.py @@ -6,84 +6,41 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse +from django.utils.decorators import method_decorator -from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm from compensation.models import EcoAccount from konova.decorators import shared_access_required, default_group_required -from konova.forms.modals import RemoveDeadlineModalForm -from konova.models import Deadline -from konova.utils.message_templates import DEADLINE_ADDED, DEADLINE_REMOVED, DEADLINE_EDITED +from konova.views.deadline import AbstractNewDeadlineView, AbstractEditDeadlineView, AbstractRemoveDeadlineView -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for editing deadlines from a compensation +class NewEcoAccountDeadlineView(AbstractNewDeadlineView): + model = EcoAccount + redirect_url = "compensation:acc:detail" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - comp = get_object_or_404(EcoAccount, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = EditDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_EDITED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for removing deadlines from a compensation +class EditEcoAccountDeadlineView(AbstractEditDeadlineView): + model = EcoAccount + redirect_url = "compensation:acc:detail" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - comp = get_object_or_404(EcoAccount, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = RemoveDeadlineModalForm(request.POST or None, instance=comp, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_REMOVED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def deadline_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for an eco account - - Args: - request (HttpRequest): The incoming request - id (str): The account's id to which the new state will be related - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - form = NewDeadlineModalForm(request.POST or None, instance=acc, request=request) - return form.process_request( - request, - msg_success=DEADLINE_ADDED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) +class RemoveEcoAccountDeadlineView(AbstractRemoveDeadlineView): + model = EcoAccount + redirect_url = "compensation:acc:detail" + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/konova/views/deadline.py b/konova/views/deadline.py new file mode 100644 index 00000000..4350073d --- /dev/null +++ b/konova/views/deadline.py @@ -0,0 +1,106 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 22.08.22 + +""" +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.views import View + +from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm +from konova.forms.modals import RemoveDeadlineModalForm +from konova.models import Deadline +from konova.utils.message_templates import DEADLINE_ADDED, DEADLINE_EDITED, DEADLINE_REMOVED + + +class AbstractNewDeadlineView(View): + model = None + redirect_url = None + + class Meta: + abstract = True + + def get(self, request, id: str): + """ Renders a form for adding new deadlines + + Args: + request (HttpRequest): The incoming request + id (str): The account's id to which the new state will be related + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + form = NewDeadlineModalForm(request.POST or None, instance=obj, request=request) + return form.process_request( + request, + msg_success=DEADLINE_ADDED, + redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + ) + + def post(self, request, id: str): + return self.get(request, id) + + +class AbstractEditDeadlineView(View): + model = None + redirect_url = None + + class Meta: + abstract = True + + def get(self, request, id: str, deadline_id: str): + """ Renders a form for editing deadlines + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + deadline_id (str): The deadline's id + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + deadline = get_object_or_404(Deadline, id=deadline_id) + form = EditDeadlineModalForm(request.POST or None, instance=obj, deadline=deadline, request=request) + return form.process_request( + request, + msg_success=DEADLINE_EDITED, + redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + ) + + def post(self, request, id: str, deadline_id: str): + return self.get(request, id, deadline_id) + + +class AbstractRemoveDeadlineView(View): + model = None + redirect_url = None + + class Meta: + abstract = True + + def get(self, request, id: str, deadline_id: str): + """ Renders a form for removing deadlines + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + deadline_id (str): The deadline's id + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + deadline = get_object_or_404(Deadline, id=deadline_id) + form = RemoveDeadlineModalForm(request.POST or None, instance=obj, deadline=deadline, request=request) + return form.process_request( + request, + msg_success=DEADLINE_REMOVED, + redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + ) + + def post(self, request, id: str, deadline_id: str): + return self.get(request, id, deadline_id) From 59b77fe567e232fe3e372451ea8cedc92d622e78 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 22 Aug 2022 08:12:55 +0200 Subject: [PATCH 27/48] Ema Deadline views * replaces ema deadline views with class based --- ema/urls.py | 8 ++-- ema/views/deadline.py | 95 ++++++++++++------------------------------- 2 files changed, 30 insertions(+), 73 deletions(-) diff --git a/ema/urls.py b/ema/urls.py index 770e24c8..eda06728 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -8,7 +8,7 @@ Created on: 19.08.21 from django.urls import path from ema.views.action import action_new_view, action_edit_view, action_remove_view -from ema.views.deadline import deadline_new_view, deadline_edit_view, deadline_remove_view +from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_view, remove_view from ema.views.log import EmaLogView @@ -39,9 +39,9 @@ urlpatterns = [ path('/action//edit', action_edit_view, name='action-edit'), path('/action//remove', action_remove_view, name='action-remove'), - path('/deadline/new', deadline_new_view, name="new-deadline"), - path('/deadline//edit', deadline_edit_view, name='deadline-edit'), - path('/deadline//remove', deadline_remove_view, name='deadline-remove'), + path('/deadline/new', NewEmaDeadlineView.as_view(), name="new-deadline"), + path('/deadline//edit', EditEmaDeadlineView.as_view(), name='deadline-edit'), + path('/deadline//remove', RemoveEmaDeadlineView.as_view(), name='deadline-remove'), path('/share/', share_view, name='share'), path('/share', create_share_view, name='share-create'), diff --git a/ema/views/deadline.py b/ema/views/deadline.py index 223ceeb0..76c2bded 100644 --- a/ema/views/deadline.py +++ b/ema/views/deadline.py @@ -6,85 +6,42 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse +from django.utils.decorators import method_decorator -from compensation.forms.modals.deadline import EditDeadlineModalForm, NewDeadlineModalForm from ema.models import Ema from konova.decorators import shared_access_required, conservation_office_group_required -from konova.forms.modals import RemoveDeadlineModalForm -from konova.models import Deadline -from konova.utils.message_templates import DEADLINE_REMOVED, DEADLINE_EDITED, DEADLINE_ADDED +from konova.views.deadline import AbstractNewDeadlineView, AbstractRemoveDeadlineView, AbstractEditDeadlineView -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def deadline_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for an EMA +class NewEmaDeadlineView(AbstractNewDeadlineView): + model = Ema + redirect_url = "ema:detail" - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id to which the new state will be related - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = NewDeadlineModalForm(request.POST or None, instance=ema, request=request) - return form.process_request( - request, - msg_success=DEADLINE_ADDED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for editing deadlines from a compensation +class EditEmaDeadlineView(AbstractEditDeadlineView): + model = Ema + redirect_url = "ema:detail" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = EditDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_EDITED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for removing deadlines from a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = RemoveDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_REMOVED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) +class RemoveEmaDeadlineView(AbstractRemoveDeadlineView): + model = Ema + redirect_url = "ema:detail" + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) From 75802c5f663be78bfed20c92e01e7757f71363cb Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 22 Aug 2022 08:27:36 +0200 Subject: [PATCH 28/48] State views * replaces function based state views with class based --- compensation/urls/compensation.py | 9 +- compensation/urls/eco_account.py | 9 +- compensation/views/compensation/state.py | 98 ++++++--------------- compensation/views/eco_account/state.py | 97 ++++++-------------- ema/urls.py | 8 +- ema/views/state.py | 97 ++++++-------------- konova/views/state.py | 107 +++++++++++++++++++++++ 7 files changed, 204 insertions(+), 221 deletions(-) create mode 100644 konova/views/state.py diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index dd089510..dcd065fd 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -14,7 +14,8 @@ from compensation.views.compensation.report import report_view from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \ RemoveCompensationDeadlineView from compensation.views.compensation.action import action_edit_view, action_new_view, action_remove_view -from compensation.views.compensation.state import state_new_view, state_remove_view, state_edit_view +from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \ + RemoveCompensationStateView from compensation.views.compensation.compensation import index_view, new_view, new_id_view, detail_view, edit_view, \ remove_view from compensation.views.compensation.log import CompensationLogView @@ -30,9 +31,9 @@ urlpatterns = [ path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), - path('/state/new', state_new_view, name='new-state'), - path('/state//edit', state_edit_view, name='state-edit'), - path('/state//remove', state_remove_view, name='state-remove'), + path('/state/new', NewCompensationStateView.as_view(), name='new-state'), + path('/state//edit', EditCompensationStateView.as_view(), name='state-edit'), + path('/state//remove', RemoveCompensationStateView.as_view(), name='state-remove'), path('/action/new', action_new_view, name='new-action'), path('/action//edit', action_edit_view, name='action-edit'), diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index 90784fb4..60ee786c 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -14,7 +14,8 @@ from compensation.views.eco_account.log import EcoAccountLogView from compensation.views.eco_account.record import EcoAccountRecordView from compensation.views.eco_account.report import report_view from compensation.views.eco_account.resubmission import EcoAccountResubmissionView -from compensation.views.eco_account.state import state_new_view, state_remove_view, state_edit_view +from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \ + RemoveEcoAccountStateView from compensation.views.eco_account.action import action_edit_view, action_new_view, action_remove_view from compensation.views.eco_account.deadline import NewEcoAccountDeadlineView, EditEcoAccountDeadlineView, \ RemoveEcoAccountDeadlineView @@ -36,9 +37,9 @@ urlpatterns = [ path('/remove', remove_view, name='remove'), path('/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'), - path('/state/new', state_new_view, name='new-state'), - path('/state//edit', state_edit_view, name='state-edit'), - path('/state//remove', state_remove_view, name='state-remove'), + path('/state/new', NewEcoAccountStateView.as_view(), name='new-state'), + path('/state//edit', EditEcoAccountStateView.as_view(), name='state-edit'), + path('/state//remove', RemoveEcoAccountStateView.as_view(), name='state-remove'), path('/action/new', action_new_view, name='new-action'), path('/action//edit', action_edit_view, name='action-edit'), diff --git a/compensation/views/compensation/state.py b/compensation/views/compensation/state.py index ca8c5d0c..136e8604 100644 --- a/compensation/views/compensation/state.py +++ b/compensation/views/compensation/state.py @@ -6,84 +6,42 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse +from django.utils.decorators import method_decorator -from compensation.forms.modals.state import EditCompensationStateModalForm, RemoveCompensationStateModalForm, \ - NewCompensationStateModalForm -from compensation.models import Compensation, CompensationState +from compensation.models import Compensation from konova.decorators import shared_access_required, default_group_required -from konova.utils.message_templates import COMPENSATION_STATE_EDITED, COMPENSATION_STATE_REMOVED, \ - COMPENSATION_STATE_ADDED +from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ + AbstractRemoveCompensationStateView -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def state_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for a compensation +class NewCompensationStateView(AbstractNewCompensationStateView): + model = Compensation + redirect_url = "compensation:detail" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id to which the new state will be related - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - form = NewCompensationStateModalForm(request.POST or None, instance=comp, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_ADDED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def state_remove_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for removing a compensation state +class EditCompensationStateView(AbstractEditCompensationStateView): + model = Compensation + redirect_url = "compensation:detail" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - state_id (str): The state's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = RemoveCompensationStateModalForm(request.POST or None, instance=comp, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_REMOVED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def state_edit_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for editing a compensation state - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - state_id (str): The state's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = EditCompensationStateModalForm(request.POST or None, instance=comp, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_EDITED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) +class RemoveCompensationStateView(AbstractRemoveCompensationStateView): + model = Compensation + redirect_url = "compensation:detail" + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/compensation/views/eco_account/state.py b/compensation/views/eco_account/state.py index 15590fc7..0591a17c 100644 --- a/compensation/views/eco_account/state.py +++ b/compensation/views/eco_account/state.py @@ -6,83 +6,42 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse +from django.utils.decorators import method_decorator -from compensation.forms.modals.state import EditCompensationStateModalForm, RemoveCompensationStateModalForm, \ - NewCompensationStateModalForm -from compensation.models import EcoAccount, CompensationState +from compensation.models import EcoAccount from konova.decorators import shared_access_required, default_group_required -from konova.utils.message_templates import COMPENSATION_STATE_EDITED, COMPENSATION_STATE_REMOVED, \ - COMPENSATION_STATE_ADDED +from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ + AbstractRemoveCompensationStateView -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def state_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for an eco account +class NewEcoAccountStateView(AbstractNewCompensationStateView): + model = EcoAccount + redirect_url = "compensation:acc:detail" - Args: - request (HttpRequest): The incoming request - id (str): The account's id to which the new state will be related - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - form = NewCompensationStateModalForm(request.POST or None, instance=acc, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_ADDED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) - -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def state_remove_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for removing a compensation state - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - state_id (str): The state's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = RemoveCompensationStateModalForm(request.POST or None, instance=acc, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_REMOVED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def state_edit_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for editing a compensation state +class EditEcoAccountStateView(AbstractEditCompensationStateView): + model = EcoAccount + redirect_url = "compensation:acc:detail" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - state_id (str): The state's id + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) - Returns: - """ - acc = get_object_or_404(EcoAccount, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = EditCompensationStateModalForm(request.POST or None, instance=acc, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_EDITED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) +class RemoveEcoAccountStateView(AbstractRemoveCompensationStateView): + model = EcoAccount + redirect_url = "compensation:acc:detail" + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/ema/urls.py b/ema/urls.py index eda06728..faeb0f2c 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -16,7 +16,7 @@ from ema.views.record import EmaRecordView from ema.views.report import report_view from ema.views.resubmission import EmaResubmissionView from ema.views.share import share_view, create_share_view -from ema.views.state import state_new_view, state_remove_view, state_edit_view +from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView app_name = "ema" urlpatterns = [ @@ -31,9 +31,9 @@ urlpatterns = [ path('/report', report_view, name='report'), path('/resub', EmaResubmissionView.as_view(), name='resubmission-create'), - path('/state/new', state_new_view, name='new-state'), - path('/state//remove', state_remove_view, name='state-remove'), - path('/state//edit', state_edit_view, name='state-edit'), + path('/state/new', NewEmaStateView.as_view(), name='new-state'), + path('/state//remove', RemoveEmaStateView.as_view(), name='state-remove'), + path('/state//edit', EditEmaStateView.as_view(), name='state-edit'), path('/action/new', action_new_view, name='new-action'), path('/action//edit', action_edit_view, name='action-edit'), diff --git a/ema/views/state.py b/ema/views/state.py index 1c7b0a44..717ae7f7 100644 --- a/ema/views/state.py +++ b/ema/views/state.py @@ -6,85 +6,42 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse +from django.utils.decorators import method_decorator -from compensation.forms.modals.state import NewCompensationStateModalForm, RemoveCompensationStateModalForm, \ - EditCompensationStateModalForm -from compensation.models import CompensationState from ema.models import Ema from konova.decorators import conservation_office_group_required, shared_access_required -from konova.utils.message_templates import COMPENSATION_STATE_ADDED, COMPENSATION_STATE_REMOVED, \ - COMPENSATION_STATE_EDITED +from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ + AbstractRemoveCompensationStateView -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def state_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for an EMA +class NewEmaStateView(AbstractNewCompensationStateView): + model = Ema + redirect_url = "ema:detail" - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id to which the new state will be related - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = NewCompensationStateModalForm(request.POST or None, instance=ema, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_ADDED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def state_remove_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for removing an EMA state +class EditEmaStateView(AbstractEditCompensationStateView): + model = Ema + redirect_url = "ema:detail" - Args: - request (HttpRequest): The incoming request - id (str): The ema id - state_id (str): The state's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = RemoveCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_REMOVED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def state_edit_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for editing an EMA state - - Args: - request (HttpRequest): The incoming request - id (str): The ema id - state_id (str): The state's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = EditCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_EDITED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) +class RemoveEmaStateView(AbstractRemoveCompensationStateView): + model = Ema + redirect_url = "ema:detail" + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/konova/views/state.py b/konova/views/state.py new file mode 100644 index 00000000..1165e08b --- /dev/null +++ b/konova/views/state.py @@ -0,0 +1,107 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 22.08.22 + +""" +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.views import View + +from compensation.forms.modals.state import NewCompensationStateModalForm, EditCompensationStateModalForm, \ + RemoveCompensationStateModalForm +from compensation.models import CompensationState +from konova.utils.message_templates import COMPENSATION_STATE_ADDED, COMPENSATION_STATE_EDITED, \ + COMPENSATION_STATE_REMOVED + + +class AbstractCompensationStateView(View): + model = None + redirect_url = None + + class Meta: + abstract = True + + +class AbstractNewCompensationStateView(AbstractCompensationStateView): + class Meta: + abstract = True + + def get(self, request, id: str): + """ Renders a form for adding new states + + Args: + request (HttpRequest): The incoming request + id (str): The object's id to which the new state will be related + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + form = NewCompensationStateModalForm(request.POST or None, instance=obj, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_ADDED, + redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + ) + + def post(self, request, id: str): + return self.get(request, id) + + +class AbstractEditCompensationStateView(AbstractCompensationStateView): + class Meta: + abstract = True + + def get(self, request, id: str, state_id: str): + """ Renders a form for editing a state + + Args: + request (HttpRequest): The incoming request + id (str): The object id + state_id (str): The state's id + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + state = get_object_or_404(CompensationState, id=state_id) + form = EditCompensationStateModalForm(request.POST or None, instance=obj, state=state, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_EDITED, + redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + ) + + def post(self, request, id: str, state_id: str): + return self.get(request, id, state_id) + + +class AbstractRemoveCompensationStateView(AbstractCompensationStateView): + class Meta: + abstract = True + + def get(self, request, id: str, state_id: str): + """ Renders a form for removing astate + + Args: + request (HttpRequest): The incoming request + id (str): The object id + state_id (str): The state's id + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + state = get_object_or_404(CompensationState, id=state_id) + form = RemoveCompensationStateModalForm(request.POST or None, instance=obj, state=state, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_REMOVED, + redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + ) + + def post(self, request, id: str, state_id: str): + return self.get(request, id, state_id) + From e66c5369a7a3f8e8639b89d722da4edbf001d159 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 22 Aug 2022 08:38:23 +0200 Subject: [PATCH 29/48] CompensationAction views * replaces function based action views with class based --- compensation/urls/compensation.py | 9 +- compensation/urls/eco_account.py | 9 +- compensation/views/compensation/action.py | 89 ++++++------------ compensation/views/eco_account/action.py | 98 ++++++-------------- ema/urls.py | 8 +- ema/views/action.py | 96 ++++++------------- konova/views/action.py | 108 ++++++++++++++++++++++ 7 files changed, 204 insertions(+), 213 deletions(-) create mode 100644 konova/views/action.py diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index dcd065fd..45a11594 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -13,7 +13,8 @@ from compensation.views.compensation.resubmission import CompensationResubmissio from compensation.views.compensation.report import report_view from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \ RemoveCompensationDeadlineView -from compensation.views.compensation.action import action_edit_view, action_new_view, action_remove_view +from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \ + RemoveCompensationActionView from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \ RemoveCompensationStateView from compensation.views.compensation.compensation import index_view, new_view, new_id_view, detail_view, edit_view, \ @@ -35,9 +36,9 @@ urlpatterns = [ path('/state//edit', EditCompensationStateView.as_view(), name='state-edit'), path('/state//remove', RemoveCompensationStateView.as_view(), name='state-remove'), - path('/action/new', action_new_view, name='new-action'), - path('/action//edit', action_edit_view, name='action-edit'), - path('/action//remove', action_remove_view, name='action-remove'), + path('/action/new', NewCompensationActionView.as_view(), name='new-action'), + path('/action//edit', EditCompensationActionView.as_view(), name='action-edit'), + path('/action//remove', RemoveCompensationActionView.as_view(), name='action-remove'), path('/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"), path('/deadline//edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'), diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index 60ee786c..01fafc47 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -16,7 +16,8 @@ from compensation.views.eco_account.report import report_view from compensation.views.eco_account.resubmission import EcoAccountResubmissionView from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \ RemoveEcoAccountStateView -from compensation.views.eco_account.action import action_edit_view, action_new_view, action_remove_view +from compensation.views.eco_account.action import NewEcoAccountActionView, EditEcoAccountActionView, \ + RemoveEcoAccountActionView from compensation.views.eco_account.deadline import NewEcoAccountDeadlineView, EditEcoAccountDeadlineView, \ RemoveEcoAccountDeadlineView from compensation.views.eco_account.share import share_view, create_share_view @@ -41,9 +42,9 @@ urlpatterns = [ path('/state//edit', EditEcoAccountStateView.as_view(), name='state-edit'), path('/state//remove', RemoveEcoAccountStateView.as_view(), name='state-remove'), - path('/action/new', action_new_view, name='new-action'), - path('/action//edit', action_edit_view, name='action-edit'), - path('/action//remove', action_remove_view, name='action-remove'), + path('/action/new', NewEcoAccountActionView.as_view(), name='new-action'), + path('/action//edit', EditEcoAccountActionView.as_view(), name='action-edit'), + path('/action//remove', RemoveEcoAccountActionView.as_view(), name='action-remove'), path('/deadline/new', NewEcoAccountDeadlineView.as_view(), name="new-deadline"), path('/deadline//edit', EditEcoAccountDeadlineView.as_view(), name='deadline-edit'), diff --git a/compensation/views/compensation/action.py b/compensation/views/compensation/action.py index 54f6bc39..1f5b9339 100644 --- a/compensation/views/compensation/action.py +++ b/compensation/views/compensation/action.py @@ -9,6 +9,7 @@ from django.contrib.auth.decorators import login_required from django.http import HttpRequest from django.shortcuts import get_object_or_404 from django.urls import reverse +from django.utils.decorators import method_decorator from compensation.forms.modals.compensation_action import RemoveCompensationActionModalForm, \ EditCompensationActionModalForm, NewCompensationActionModalForm @@ -16,74 +17,38 @@ from compensation.models import Compensation, CompensationAction from konova.decorators import shared_access_required, default_group_required from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_EDITED, \ COMPENSATION_ACTION_ADDED +from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ + AbstractRemoveCompensationActionView -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def action_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new actions for a compensation +class NewCompensationActionView(AbstractNewCompensationActionView): + model = Compensation + redirect_url = "compensation:detail" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id to which the new state will be related - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - form = NewCompensationActionModalForm(request.POST or None, instance=comp, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_ADDED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def action_edit_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for editing actions for a compensation +class EditCompensationActionView(AbstractEditCompensationActionView): + model = Compensation + redirect_url = "compensation:detail" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - action_id (str): The action's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = EditCompensationActionModalForm(request.POST or None, instance=comp, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_EDITED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def action_remove_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for removing a compensation action - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - id (str): The action's id - - Returns: - - """ - comp = get_object_or_404(Compensation, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = RemoveCompensationActionModalForm(request.POST or None, instance=comp, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_REMOVED, - redirect_url=reverse("compensation:detail", args=(id,)) + "#related_data" - ) +class RemoveCompensationActionView(AbstractRemoveCompensationActionView): + model = Compensation + redirect_url = "compensation:detail" + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/compensation/views/eco_account/action.py b/compensation/views/eco_account/action.py index c7eb2185..e459def7 100644 --- a/compensation/views/eco_account/action.py +++ b/compensation/views/eco_account/action.py @@ -6,84 +6,42 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse +from django.utils.decorators import method_decorator -from compensation.forms.modals.compensation_action import EditCompensationActionModalForm, \ - RemoveCompensationActionModalForm, NewCompensationActionModalForm -from compensation.models import EcoAccount, CompensationAction +from compensation.models import EcoAccount from konova.decorators import shared_access_required, default_group_required -from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, COMPENSATION_ACTION_REMOVED, \ - COMPENSATION_ACTION_ADDED +from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ + AbstractRemoveCompensationActionView -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def action_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new actions for an eco account +class NewEcoAccountActionView(AbstractNewCompensationActionView): + model = EcoAccount + redirect_url = "compensation:acc:detail" - Args: - request (HttpRequest): The incoming request - id (str): The account's id to which the new state will be related - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - form = NewCompensationActionModalForm(request.POST or None, instance=acc, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_ADDED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def action_remove_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for removing a compensation action +class EditEcoAccountActionView(AbstractEditCompensationActionView): + model = EcoAccount + redirect_url = "compensation:acc:detail" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - id (str): The action's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = RemoveCompensationActionModalForm(request.POST or None, instance=acc, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_REMOVED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def action_edit_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for editing a compensation action - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - id (str): The action's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = EditCompensationActionModalForm(request.POST or None, instance=acc, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_EDITED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) +class RemoveEcoAccountActionView(AbstractRemoveCompensationActionView): + model = EcoAccount + redirect_url = "compensation:acc:detail" + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/ema/urls.py b/ema/urls.py index faeb0f2c..a9c6dc41 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -7,7 +7,7 @@ Created on: 19.08.21 """ from django.urls import path -from ema.views.action import action_new_view, action_edit_view, action_remove_view +from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_view, remove_view @@ -35,9 +35,9 @@ urlpatterns = [ path('/state//remove', RemoveEmaStateView.as_view(), name='state-remove'), path('/state//edit', EditEmaStateView.as_view(), name='state-edit'), - path('/action/new', action_new_view, name='new-action'), - path('/action//edit', action_edit_view, name='action-edit'), - path('/action//remove', action_remove_view, name='action-remove'), + path('/action/new', NewEmaActionView.as_view(), name='new-action'), + path('/action//edit', EditEmaActionView.as_view(), name='action-edit'), + path('/action//remove', RemoveEmaActionView.as_view(), name='action-remove'), path('/deadline/new', NewEmaDeadlineView.as_view(), name="new-deadline"), path('/deadline//edit', EditEmaDeadlineView.as_view(), name='deadline-edit'), diff --git a/ema/views/action.py b/ema/views/action.py index bd13646c..bae950c6 100644 --- a/ema/views/action.py +++ b/ema/views/action.py @@ -6,84 +6,42 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse +from django.utils.decorators import method_decorator -from compensation.forms.modals.compensation_action import RemoveCompensationActionModalForm, \ - EditCompensationActionModalForm, NewCompensationActionModalForm -from compensation.models import CompensationAction from ema.models import Ema from konova.decorators import shared_access_required, conservation_office_group_required -from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_EDITED, \ - COMPENSATION_ACTION_ADDED +from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ + AbstractRemoveCompensationActionView -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def action_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new actions for an EMA +class NewEmaActionView(AbstractNewCompensationActionView): + model = Ema + redirect_url = "ema:detail" - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id to which the new state will be related - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = NewCompensationActionModalForm(request.POST or None, instance=ema, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_ADDED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def action_edit_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for editing an actions for an EMA +class EditEmaActionView(AbstractEditCompensationActionView): + model = Ema + redirect_url = "ema:detail" - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id - action_id (str): The action id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = EditCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_EDITED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def action_remove_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for removing an EMA action +class RemoveEmaActionView(AbstractRemoveCompensationActionView): + model = Ema + redirect_url = "ema:detail" - Args: - request (HttpRequest): The incoming request - id (str): The ema id - id (str): The action's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = RemoveCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_REMOVED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/konova/views/action.py b/konova/views/action.py new file mode 100644 index 00000000..5699a963 --- /dev/null +++ b/konova/views/action.py @@ -0,0 +1,108 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 22.08.22 + +""" +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.views import View + +from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \ + EditCompensationActionModalForm, RemoveCompensationActionModalForm +from compensation.models import CompensationAction +from konova.utils.message_templates import COMPENSATION_STATE_ADDED, COMPENSATION_STATE_EDITED, \ + COMPENSATION_STATE_REMOVED + + +class AbstractCompensationActionView(View): + model = None + redirect_url = None + + class Meta: + abstract = True + + +class AbstractNewCompensationActionView(AbstractCompensationActionView): + class Meta: + abstract = True + + def get(self, request, id: str): + """ Renders a form for adding new actions + + Args: + request (HttpRequest): The incoming request + id (str): The object's id to which the new action will be related + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + form = NewCompensationActionModalForm(request.POST or None, instance=obj, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_ADDED, + redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + ) + + def post(self, request, id: str): + return self.get(request, id) + + +class AbstractEditCompensationActionView(AbstractCompensationActionView): + class Meta: + abstract = True + + def get(self, request, id: str, action_id: str): + """ Renders a form for editing a action + + Args: + request (HttpRequest): The incoming request + id (str): The object id + action_id (str): The action's id + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + action = get_object_or_404(CompensationAction, id=action_id) + form = EditCompensationActionModalForm(request.POST or None, instance=obj, action=action, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_EDITED, + redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + ) + + def post(self, request, id: str, action_id: str): + return self.get(request, id, action_id) + + +class AbstractRemoveCompensationActionView(AbstractCompensationActionView): + class Meta: + abstract = True + + def get(self, request, id: str, action_id: str): + """ Renders a form for removing aaction + + Args: + request (HttpRequest): The incoming request + id (str): The object id + action_id (str): The action's id + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + action = get_object_or_404(CompensationAction, id=action_id) + form = RemoveCompensationActionModalForm(request.POST or None, instance=obj, action=action, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_REMOVED, + redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + ) + + def post(self, request, id: str, action_id: str): + return self.get(request, id, action_id) + + From 09402611dc72a21384c7531574c672794ca58de3 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 22 Aug 2022 10:17:49 +0200 Subject: [PATCH 30/48] EcoAccount views * splits compensation/views/eco_account.py (+700 lines) into separate files in new module * view files can now be found in /compensation/views/eco_account/... --- compensation/urls/eco_account.py | 9 +- compensation/views/eco_account/deduction.py | 108 +++++----------- intervention/urls.py | 9 +- intervention/views/deduction.py | 106 +++++---------- konova/utils/message_templates.py | 1 + konova/views/deduction.py | 135 ++++++++++++++++++++ 6 files changed, 209 insertions(+), 159 deletions(-) create mode 100644 konova/views/deduction.py diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index 01fafc47..af430a93 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -23,7 +23,8 @@ from compensation.views.eco_account.deadline import NewEcoAccountDeadlineView, E from compensation.views.eco_account.share import share_view, create_share_view from compensation.views.eco_account.document import GetEcoAccountDocumentView, NewEcoAccountDocumentView, \ EditEcoAccountDocumentView, RemoveEcoAccountDocumentView -from compensation.views.eco_account.deduction import deduction_edit_view, deduction_remove_view, new_deduction_view +from compensation.views.eco_account.deduction import NewEcoAccountDeductionView, EditEcoAccountDeductionView, \ + RemoveEcoAccountDeductionView app_name = "acc" urlpatterns = [ @@ -60,9 +61,9 @@ urlpatterns = [ path('/document//remove/', RemoveEcoAccountDocumentView.as_view(), name='remove-doc'), # Eco-account deductions - path('/deduction//remove', deduction_remove_view, name='remove-deduction'), - path('/deduction//edit', deduction_edit_view, name='edit-deduction'), - path('/deduct/new', new_deduction_view, name='new-deduction'), + path('/deduction//remove', RemoveEcoAccountDeductionView.as_view(), name='remove-deduction'), + path('/deduction//edit', EditEcoAccountDeductionView.as_view(), name='edit-deduction'), + path('/deduct/new', NewEcoAccountDeductionView.as_view(), name='new-deduction'), # Autocomplete path("atcmplt/eco-accounts", EcoAccountAutocomplete.as_view(), name="autocomplete"), diff --git a/compensation/views/eco_account/deduction.py b/compensation/views/eco_account/deduction.py index e9b1dcb9..c7598bb8 100644 --- a/compensation/views/eco_account/deduction.py +++ b/compensation/views/eco_account/deduction.py @@ -6,96 +6,50 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.core.exceptions import ObjectDoesNotExist -from django.http import HttpRequest, Http404 -from django.shortcuts import get_object_or_404 -from django.urls import reverse +from django.http import Http404 +from django.utils.decorators import method_decorator from compensation.models import EcoAccount -from intervention.forms.modals.deduction import EditEcoAccountDeductionModalForm, RemoveEcoAccountDeductionModalForm, \ - NewEcoAccountDeductionModalForm from konova.decorators import default_group_required -from konova.utils.message_templates import DEDUCTION_EDITED, DEDUCTION_REMOVED, DEDUCTION_ADDED +from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView -@login_required -@default_group_required -def new_deduction_view(request: HttpRequest, id: str): - """ Renders a modal form view for creating deductions +class NewEcoAccountDeductionView(AbstractNewDeductionView): + model = EcoAccount + redirect_url = "compensation:acc:detail" - Args: - request (HttpRequest): THe incoming request - id (str): The eco account's id + @method_decorator(login_required) + @method_decorator(default_group_required) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - if not acc.recorded: - raise Http404() - form = NewEcoAccountDeductionModalForm(request.POST or None, instance=acc, request=request) - return form.process_request( - request, - msg_success=DEDUCTION_ADDED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) + def _custom_check(self, obj): + if not obj.recorded: + raise Http404() -@login_required -@default_group_required -def deduction_remove_view(request: HttpRequest, id: str, deduction_id: str): - """ Renders a modal view for removing deductions +class EditEcoAccountDeductionView(AbstractEditDeductionView): + def _custom_check(self, obj): + pass - Args: - request (HttpRequest): The incoming request - id (str): The eco account's id - deduction_id (str): The deduction's id + model = EcoAccount + redirect_url = "compensation:acc:detail" - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - try: - eco_deduction = acc.deductions.get(id=deduction_id) - if not eco_deduction.intervention.is_shared_with(request.user): - raise ObjectDoesNotExist() - except ObjectDoesNotExist: - raise Http404("Unknown deduction") - - form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=acc, deduction=eco_deduction, request=request) - return form.process_request( - request=request, - msg_success=DEDUCTION_REMOVED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -def deduction_edit_view(request: HttpRequest, id: str, deduction_id: str): - """ Renders a modal view for editing deductions +class RemoveEcoAccountDeductionView(AbstractRemoveDeductionView): + def _custom_check(self, obj): + pass - Args: - request (HttpRequest): The incoming request - id (str): The eco account's id - deduction_id (str): The deduction's id - - Returns: - - """ - acc = get_object_or_404(EcoAccount, id=id) - try: - eco_deduction = acc.deductions.get(id=deduction_id) - if not eco_deduction.intervention.is_shared_with(request.user): - raise ObjectDoesNotExist - except ObjectDoesNotExist: - raise Http404("Unknown deduction") - - form = EditEcoAccountDeductionModalForm(request.POST or None, instance=acc, deduction=eco_deduction, request=request) - return form.process_request( - request=request, - msg_success=DEDUCTION_EDITED, - redirect_url=reverse("compensation:acc:detail", args=(id,)) + "#related_data" - ) + model = EcoAccount + redirect_url = "compensation:acc:detail" + @method_decorator(login_required) + @method_decorator(default_group_required) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/intervention/urls.py b/intervention/urls.py index 20bd5c16..69c5e767 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -10,7 +10,8 @@ from django.urls import path from intervention.autocomplete.intervention import InterventionAutocomplete from intervention.views.check import check_view from intervention.views.compensation import remove_compensation_view -from intervention.views.deduction import new_deduction_view, edit_deduction_view, remove_deduction_view +from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \ + RemoveInterventionDeductionView from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \ RemoveInterventionDocumentView, EditInterventionDocumentView from intervention.views.intervention import index_view, new_view, new_id_view, detail_view, edit_view, remove_view @@ -48,9 +49,9 @@ urlpatterns = [ path('/document//edit/', EditInterventionDocumentView.as_view(), name='edit-doc'), # Deductions - path('/deduction/new', new_deduction_view, name='new-deduction'), - path('/deduction//edit', edit_deduction_view, name='edit-deduction'), - path('/deduction//remove', remove_deduction_view, name='remove-deduction'), + path('/deduction/new', NewInterventionDeductionView.as_view(), name='new-deduction'), + path('/deduction//edit', EditInterventionDeductionView.as_view(), name='edit-deduction'), + path('/deduction//remove', RemoveInterventionDeductionView.as_view(), name='remove-deduction'), # Revocation routes path('/revocation/new', new_revocation_view, name='new-revocation'), diff --git a/intervention/views/deduction.py b/intervention/views/deduction.py index 2fb841c1..962fe807 100644 --- a/intervention/views/deduction.py +++ b/intervention/views/deduction.py @@ -6,92 +6,50 @@ Created on: 19.08.22 """ from django.contrib.auth.decorators import login_required -from django.core.exceptions import ObjectDoesNotExist -from django.http import HttpRequest, Http404 -from django.shortcuts import get_object_or_404 -from django.urls import reverse +from django.utils.decorators import method_decorator -from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, RemoveEcoAccountDeductionModalForm, \ - EditEcoAccountDeductionModalForm from intervention.models import Intervention from konova.decorators import default_group_required, shared_access_required -from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_REMOVED, DEDUCTION_EDITED +from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def new_deduction_view(request: HttpRequest, id: str): - """ Renders a modal form view for creating deductions +class NewInterventionDeductionView(AbstractNewDeductionView): + def _custom_check(self, obj): + pass - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id which shall benefit from this deduction + model = Intervention + redirect_url = "intervention:detail" - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - form = NewEcoAccountDeductionModalForm(request.POST or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=DEDUCTION_ADDED, - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data", - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def remove_deduction_view(request: HttpRequest, id: str, deduction_id: str): - """ Renders a modal view for removing deductions +class EditInterventionDeductionView(AbstractEditDeductionView): + def _custom_check(self, obj): + pass - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - deduction_id (str): The deduction's id + model = Intervention + redirect_url = "intervention:detail" - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - try: - eco_deduction = intervention.deductions.get(id=deduction_id) - except ObjectDoesNotExist: - raise Http404("Unknown deduction") - - form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=intervention, deduction=eco_deduction, request=request) - return form.process_request( - request=request, - msg_success=DEDUCTION_REMOVED, - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" - ) + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def edit_deduction_view(request: HttpRequest, id: str, deduction_id: str): - """ Renders a modal view for removing deductions +class RemoveInterventionDeductionView(AbstractRemoveDeductionView): + def _custom_check(self, obj): + pass - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - deduction_id (str): The deduction's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - try: - eco_deduction = intervention.deductions.get(id=deduction_id) - except ObjectDoesNotExist: - raise Http404("Unknown deduction") - - form = EditEcoAccountDeductionModalForm(request.POST or None, instance=intervention, deduction=eco_deduction, request=request) - return form.process_request( - request=request, - msg_success=DEDUCTION_EDITED, - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" - ) + model = Intervention + redirect_url = "intervention:detail" + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py index f6e3ca18..e144a97f 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -51,6 +51,7 @@ COMPENSATION_ACTION_REMOVED = _("Action removed") DEDUCTION_ADDED = _("Deduction added") DEDUCTION_EDITED = _("Deduction edited") DEDUCTION_REMOVED = _("Deduction removed") +DEDUCTION_UNKNOWN = _("Unknown deduction") # DEADLINE DEADLINE_ADDED = _("Deadline added") diff --git a/konova/views/deduction.py b/konova/views/deduction.py new file mode 100644 index 00000000..f31a7015 --- /dev/null +++ b/konova/views/deduction.py @@ -0,0 +1,135 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 22.08.22 + +""" +from django.core.exceptions import ObjectDoesNotExist +from django.http import Http404 +from django.shortcuts import get_object_or_404 +from django.urls import reverse +from django.views import View + +from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \ + RemoveEcoAccountDeductionModalForm +from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_EDITED, DEDUCTION_REMOVED, DEDUCTION_UNKNOWN + + +class AbstractDeductionView(View): + model = None + redirect_url = None + + def _custom_check(self, obj): + """ + Can be used by inheriting classes to provide custom checks before further processing + + """ + raise NotImplementedError("Must be implemented in subclasses") + + +class AbstractNewDeductionView(AbstractDeductionView): + + class Meta: + abstract = True + + def get(self, request, id: str): + """ Renders a modal form view for creating deductions + + Args: + request (HttpRequest): The incoming request + id (str): The obj's id which shall benefit from this deduction + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + self._custom_check(obj) + form = NewEcoAccountDeductionModalForm(request.POST or None, instance=obj, request=request) + return form.process_request( + request, + msg_success=DEDUCTION_ADDED, + redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data", + ) + + def post(self, request, id: str): + return self.get(request, id) + + +class AbstractEditDeductionView(AbstractDeductionView): + + def _custom_check(self, obj): + pass + + class Meta: + abstract = True + + def get(self, request, id: str, deduction_id: str): + """ Renders a modal view for editing deductions + + Args: + request (HttpRequest): The incoming request + id (str): The object's id + deduction_id (str): The deduction's id + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + self._custom_check(obj) + try: + eco_deduction = obj.deductions.get(id=deduction_id) + if not eco_deduction.intervention.is_shared_with(request.user): + raise ObjectDoesNotExist + except ObjectDoesNotExist: + raise Http404(DEDUCTION_UNKNOWN) + + form = EditEcoAccountDeductionModalForm(request.POST or None, instance=obj, deduction=eco_deduction, + request=request) + return form.process_request( + request=request, + msg_success=DEDUCTION_EDITED, + redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + ) + + def post(self, request, id: str, deduction_id: str): + return self.get(request, id, deduction_id) + + +class AbstractRemoveDeductionView(AbstractDeductionView): + + def _custom_check(self, obj): + pass + + class Meta: + abstract = True + + def get(self, request, id: str, deduction_id: str): + """ Renders a modal view for removing deductions + + Args: + request (HttpRequest): The incoming request + id (str): The object's id + deduction_id (str): The deduction's id + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + self._custom_check(obj) + try: + eco_deduction = obj.deductions.get(id=deduction_id) + if not eco_deduction.intervention.is_shared_with(request.user): + raise ObjectDoesNotExist() + except ObjectDoesNotExist: + raise Http404(DEDUCTION_UNKNOWN) + form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=obj, deduction=eco_deduction, + request=request) + return form.process_request( + request=request, + msg_success=DEDUCTION_REMOVED, + redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + ) + + def post(self, request, id: str, deduction_id: str): + return self.get(request, id, deduction_id) From fc19a4de5299609445577934b9c8ee8be9f62949 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 22 Aug 2022 10:58:07 +0200 Subject: [PATCH 31/48] Share views * replaces function based share views with class based * improves team-share autocomplete search * renames internal share url names --- compensation/models/eco_account.py | 2 +- .../detail/eco_account/includes/controls.html | 2 +- compensation/urls/eco_account.py | 6 +- compensation/views/eco_account/share.py | 76 ++++------------ ema/models/ema.py | 2 +- .../ema/detail/includes/controls.html | 2 +- ema/tests/test_views.py | 4 +- ema/urls.py | 6 +- ema/views/share.py | 75 ++++------------ intervention/models/intervention.py | 2 +- .../detail/includes/controls.html | 2 +- intervention/tests/test_views.py | 4 +- intervention/urls.py | 6 +- intervention/views/share.py | 76 ++++------------ konova/utils/message_templates.py | 1 + konova/views/share.py | 88 +++++++++++++++++++ user/autocomplete/share.py | 8 +- 17 files changed, 158 insertions(+), 204 deletions(-) create mode 100644 konova/views/share.py diff --git a/compensation/models/eco_account.py b/compensation/models/eco_account.py index 42a202f7..dc9e336f 100644 --- a/compensation/models/eco_account.py +++ b/compensation/models/eco_account.py @@ -160,7 +160,7 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix Returns: """ - return reverse("compensation:acc:share", args=(self.id, self.access_token)) + return reverse("compensation:acc:share-token", args=(self.id, self.access_token)) def send_notification_mail_on_deduction_change(self, data_change: dict): """ Sends notification mails for changes on the deduction diff --git a/compensation/templates/compensation/detail/eco_account/includes/controls.html b/compensation/templates/compensation/detail/eco_account/includes/controls.html index 42ce6067..c26883aa 100644 --- a/compensation/templates/compensation/detail/eco_account/includes/controls.html +++ b/compensation/templates/compensation/detail/eco_account/includes/controls.html @@ -15,7 +15,7 @@ - {% if is_ets_member %} diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index af430a93..beaae8d9 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -20,7 +20,7 @@ from compensation.views.eco_account.action import NewEcoAccountActionView, EditE RemoveEcoAccountActionView from compensation.views.eco_account.deadline import NewEcoAccountDeadlineView, EditEcoAccountDeadlineView, \ RemoveEcoAccountDeadlineView -from compensation.views.eco_account.share import share_view, create_share_view +from compensation.views.eco_account.share import EcoAccountShareByTokenView, EcoAccountShareFormView from compensation.views.eco_account.document import GetEcoAccountDocumentView, NewEcoAccountDocumentView, \ EditEcoAccountDocumentView, RemoveEcoAccountDocumentView from compensation.views.eco_account.deduction import NewEcoAccountDeductionView, EditEcoAccountDeductionView, \ @@ -51,8 +51,8 @@ urlpatterns = [ path('/deadline//edit', EditEcoAccountDeadlineView.as_view(), name='deadline-edit'), path('/deadline//remove', RemoveEcoAccountDeadlineView.as_view(), name='deadline-remove'), - path('/share/', share_view, name='share'), - path('/share', create_share_view, name='share-create'), + path('/share/', EcoAccountShareByTokenView.as_view(), name='share-token'), + path('/share', EcoAccountShareFormView.as_view(), name='share-form'), # Documents path('/document/new/', NewEcoAccountDocumentView.as_view(), name='new-doc'), diff --git a/compensation/views/eco_account/share.py b/compensation/views/eco_account/share.py index 7b1916e4..c2f2e53f 100644 --- a/compensation/views/eco_account/share.py +++ b/compensation/views/eco_account/share.py @@ -5,74 +5,28 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, redirect -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from compensation.models import EcoAccount -from intervention.forms.modals.share import ShareModalForm from konova.decorators import shared_access_required, default_group_required +from konova.views.share import AbstractShareByTokenView, AbstractShareFormView -@login_required -def share_view(request: HttpRequest, id: str, token: str): - """ Performs sharing of an eco account +class EcoAccountShareByTokenView(AbstractShareByTokenView): + model = EcoAccount + redirect_url = "compensation:acc:detail" - If token given in url is not valid, the user will be redirected to the dashboard - - Args: - request (HttpRequest): The incoming request - id (str): EcoAccount's id - token (str): Access token for EcoAccount - - Returns: - - """ - user = request.user - obj = get_object_or_404(EcoAccount, id=id) - # Check tokens - if obj.access_token == token: - # Send different messages in case user has already been added to list of sharing users - if obj.is_shared_with(user): - messages.info( - request, - _("{} has already been shared with you").format(obj.identifier) - ) - else: - messages.success( - request, - _("{} has been shared with you").format(obj.identifier) - ) - obj.share_with_user(user) - return redirect("compensation:acc:detail", id=id) - else: - messages.error( - request, - _("Share link invalid"), - extra_tags="danger", - ) - return redirect("home") + @method_decorator(login_required) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def create_share_view(request: HttpRequest, id: str): - """ Renders sharing form for an eco account - - Args: - request (HttpRequest): The incoming request - id (str): EcoAccount's id - - Returns: - - """ - obj = get_object_or_404(EcoAccount, id=id) - form = ShareModalForm(request.POST or None, instance=obj, request=request) - return form.process_request( - request, - msg_success=_("Share settings updated") - ) +class EcoAccountShareFormView(AbstractShareFormView): + model = EcoAccount + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/ema/models/ema.py b/ema/models/ema.py index abec7c43..a7172da8 100644 --- a/ema/models/ema.py +++ b/ema/models/ema.py @@ -103,7 +103,7 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, Pik Returns: """ - return reverse("ema:share", args=(self.id, self.access_token)) + return reverse("ema:share-token", args=(self.id, self.access_token)) class EmaDocument(AbstractDocument): diff --git a/ema/templates/ema/detail/includes/controls.html b/ema/templates/ema/detail/includes/controls.html index a16071bf..182be481 100644 --- a/ema/templates/ema/detail/includes/controls.html +++ b/ema/templates/ema/detail/includes/controls.html @@ -15,7 +15,7 @@ - {% if is_ets_member %} diff --git a/ema/tests/test_views.py b/ema/tests/test_views.py index b0cba9a3..627753ac 100644 --- a/ema/tests/test_views.py +++ b/ema/tests/test_views.py @@ -49,8 +49,8 @@ class EmaViewTestCase(CompensationViewTestCase): self.log_url = reverse("ema:log", args=(self.ema.id,)) self.edit_url = reverse("ema:edit", args=(self.ema.id,)) self.remove_url = reverse("ema:remove", args=(self.ema.id,)) - self.share_url = reverse("ema:share", args=(self.ema.id, self.ema.access_token,)) - self.share_create_url = reverse("ema:share-create", args=(self.ema.id,)) + self.share_url = reverse("ema:share-token", args=(self.ema.id, self.ema.access_token,)) + self.share_create_url = reverse("ema:share-form", args=(self.ema.id,)) self.record_url = reverse("ema:record", args=(self.ema.id,)) self.report_url = reverse("ema:report", args=(self.ema.id,)) self.new_doc_url = reverse("ema:new-doc", args=(self.ema.id,)) diff --git a/ema/urls.py b/ema/urls.py index a9c6dc41..bff7c41d 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -15,7 +15,7 @@ from ema.views.log import EmaLogView from ema.views.record import EmaRecordView from ema.views.report import report_view from ema.views.resubmission import EmaResubmissionView -from ema.views.share import share_view, create_share_view +from ema.views.share import EmaShareFormView, EmaShareByTokenView from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView app_name = "ema" @@ -43,8 +43,8 @@ urlpatterns = [ path('/deadline//edit', EditEmaDeadlineView.as_view(), name='deadline-edit'), path('/deadline//remove', RemoveEmaDeadlineView.as_view(), name='deadline-remove'), - path('/share/', share_view, name='share'), - path('/share', create_share_view, name='share-create'), + path('/share/', EmaShareByTokenView.as_view(), name='share-token'), + path('/share', EmaShareFormView.as_view(), name='share-form'), # Documents path('/document/new/', NewEmaDocumentView.as_view(), name='new-doc'), diff --git a/ema/views/share.py b/ema/views/share.py index 83aae91d..536fe31e 100644 --- a/ema/views/share.py +++ b/ema/views/share.py @@ -5,73 +5,28 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, redirect -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator from ema.models import Ema -from intervention.forms.modals.share import ShareModalForm from konova.decorators import conservation_office_group_required, shared_access_required +from konova.views.share import AbstractShareByTokenView, AbstractShareFormView -@login_required -def share_view(request: HttpRequest, id: str, token: str): - """ Performs sharing of an ema +class EmaShareByTokenView(AbstractShareByTokenView): + model = Ema + redirect_url = "ema:detail" - If token given in url is not valid, the user will be redirected to the dashboard - - Args: - request (HttpRequest): The incoming request - id (str): EMA's id - token (str): Access token for EMA - - Returns: - - """ - user = request.user - obj = get_object_or_404(Ema, id=id) - # Check tokens - if obj.access_token == token: - # Send different messages in case user has already been added to list of sharing users - if obj.is_shared_with(user): - messages.info( - request, - _("{} has already been shared with you").format(obj.identifier) - ) - else: - messages.success( - request, - _("{} has been shared with you").format(obj.identifier) - ) - obj.share_with_user(user) - return redirect("ema:detail", id=id) - else: - messages.error( - request, - _("Share link invalid"), - extra_tags="danger", - ) - return redirect("home") + @method_decorator(login_required) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def create_share_view(request: HttpRequest, id: str): - """ Renders sharing form for an Ema +class EmaShareFormView(AbstractShareFormView): + model = Ema - Args: - request (HttpRequest): The incoming request - id (str): Ema's id - - Returns: - - """ - obj = get_object_or_404(Ema, id=id) - form = ShareModalForm(request.POST or None, instance=obj, request=request) - return form.process_request( - request, - msg_success=_("Share settings updated") - ) + @method_decorator(login_required) + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index ea561c5b..91c0ddb6 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -337,7 +337,7 @@ class Intervention(BaseObject, Returns: """ - return reverse("intervention:share", args=(self.id, self.access_token)) + return reverse("intervention:share-token", args=(self.id, self.access_token)) def remove_payment(self, form): """ Removes a Payment from the intervention diff --git a/intervention/templates/intervention/detail/includes/controls.html b/intervention/templates/intervention/detail/includes/controls.html index 7af2165b..7008c101 100644 --- a/intervention/templates/intervention/detail/includes/controls.html +++ b/intervention/templates/intervention/detail/includes/controls.html @@ -15,7 +15,7 @@ - {% if is_zb_member %} diff --git a/intervention/tests/test_views.py b/intervention/tests/test_views.py index a049f3e7..e552dbe6 100644 --- a/intervention/tests/test_views.py +++ b/intervention/tests/test_views.py @@ -31,8 +31,8 @@ class InterventionViewTestCase(BaseViewTestCase): self.log_url = reverse("intervention:log", args=(self.intervention.id,)) self.edit_url = reverse("intervention:edit", args=(self.intervention.id,)) self.remove_url = reverse("intervention:remove", args=(self.intervention.id,)) - self.share_url = reverse("intervention:share", args=(self.intervention.id, self.intervention.access_token,)) - self.share_create_url = reverse("intervention:share-create", args=(self.intervention.id,)) + self.share_url = reverse("intervention:share-token", args=(self.intervention.id, self.intervention.access_token,)) + self.share_create_url = reverse("intervention:share-form", args=(self.intervention.id,)) self.run_check_url = reverse("intervention:check", args=(self.intervention.id,)) self.record_url = reverse("intervention:record", args=(self.intervention.id,)) self.report_url = reverse("intervention:report", args=(self.intervention.id,)) diff --git a/intervention/urls.py b/intervention/urls.py index 69c5e767..8a148197 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -21,7 +21,7 @@ from intervention.views.report import report_view from intervention.views.resubmission import InterventionResubmissionView from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \ get_revocation_view -from intervention.views.share import share_view, create_share_view +from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView app_name = "intervention" urlpatterns = [ @@ -32,8 +32,8 @@ urlpatterns = [ path('/log', InterventionLogView.as_view(), name='log'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), - path('/share/', share_view, name='share'), - path('/share', create_share_view, name='share-create'), + path('/share/', InterventionShareByTokenView.as_view(), name='share-token'), + path('/share', InterventionShareFormView.as_view(), name='share-form'), path('/check', check_view, name='check'), path('/record', InterventionRecordView.as_view(), name='record'), path('/report', report_view, name='report'), diff --git a/intervention/views/share.py b/intervention/views/share.py index ba9cdd3a..f78d65b2 100644 --- a/intervention/views/share.py +++ b/intervention/views/share.py @@ -5,74 +5,28 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, redirect -from django.utils.translation import gettext_lazy as _ +from django.utils.decorators import method_decorator -from intervention.forms.modals.share import ShareModalForm from intervention.models import Intervention from konova.decorators import default_group_required, shared_access_required +from konova.views.share import AbstractShareByTokenView, AbstractShareFormView -@login_required -def share_view(request: HttpRequest, id: str, token: str): - """ Performs sharing of an intervention +class InterventionShareByTokenView(AbstractShareByTokenView): + model = Intervention + redirect_url = "intervention:detail" - If token given in url is not valid, the user will be redirected to the dashboard - - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id - token (str): Access token for intervention - - Returns: - - """ - user = request.user - intervention = get_object_or_404(Intervention, id=id) - # Check tokens - if intervention.access_token == token: - # Send different messages in case user has already been added to list of sharing users - if intervention.is_shared_with(user): - messages.info( - request, - _("{} has already been shared with you").format(intervention.identifier) - ) - else: - messages.success( - request, - _("{} has been shared with you").format(intervention.identifier) - ) - intervention.share_with_user(user) - return redirect("intervention:detail", id=id) - else: - messages.error( - request, - _("Share link invalid"), - extra_tags="danger", - ) - return redirect("home") + @method_decorator(login_required) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def create_share_view(request: HttpRequest, id: str): - """ Renders sharing form for an intervention - - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - form = ShareModalForm(request.POST or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=_("Share settings updated") - ) +class InterventionShareFormView(AbstractShareFormView): + model = Intervention + @method_decorator(login_required) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py index e144a97f..6ddbba99 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -22,6 +22,7 @@ RECORDED_BLOCKS_EDIT = _("Entry is recorded. To edit data, the entry first needs # SHARE DATA_UNSHARED = _("This data is not shared with you") DATA_UNSHARED_EXPLANATION = _("Remember: This data has not been shared with you, yet. This means you can only read but can not edit or perform any actions like running a check or recording.") +DATA_SHARE_SET = _("Share settings updated") # FILES FILE_TYPE_UNSUPPORTED = _("Unsupported file type") diff --git a/konova/views/share.py b/konova/views/share.py new file mode 100644 index 00000000..abcbecaa --- /dev/null +++ b/konova/views/share.py @@ -0,0 +1,88 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 22.08.22 + +""" +from django.contrib import messages +from django.shortcuts import get_object_or_404, redirect +from django.views import View +from django.utils.translation import gettext_lazy as _ + +from intervention.forms.modals.share import ShareModalForm +from konova.utils.message_templates import DATA_SHARE_SET + + +class AbstractShareByTokenView(View): + model = None + redirect_url = None + + class Meta: + abstract = True + + def get(self, request, id: str, token: str): + + """ Performs sharing of an intervention + + If token given in url is not valid, the user will be redirected to the dashboard + + Args: + request (HttpRequest): The incoming request + id (str): Object's id + token (str): Access token for object + + Returns: + + """ + user = request.user + obj = get_object_or_404(self.model, id=id) + # Check tokens + if obj.access_token == token: + # Send different messages in case user has already been added to list of sharing users + if obj.is_shared_with(user): + messages.info( + request, + _("{} has already been shared with you").format(obj.identifier) + ) + else: + messages.success( + request, + _("{} has been shared with you").format(obj.identifier) + ) + obj.share_with_user(user) + return redirect(self.redirect_url, id=id) + else: + messages.error( + request, + _("Share link invalid"), + extra_tags="danger", + ) + return redirect("home") + + +class AbstractShareFormView(View): + model = None + + class Meta: + abstract = True + + def get(self, request, id: str): + """ Renders sharing form + + Args: + request (HttpRequest): The incoming request + id (str): Object's id + + Returns: + + """ + obj = get_object_or_404(self.model, id=id) + form = ShareModalForm(request.POST or None, instance=obj, request=request) + return form.process_request( + request, + msg_success=DATA_SHARE_SET + ) + + def post(self, request, id: str): + return self.get(request, id) diff --git a/user/autocomplete/share.py b/user/autocomplete/share.py index de634111..9331873d 100644 --- a/user/autocomplete/share.py +++ b/user/autocomplete/share.py @@ -42,9 +42,11 @@ class ShareTeamAutocomplete(Select2QuerySetView): ) if self.q: # Due to privacy concerns only a full username match will return the proper user entry - qs = qs.filter( - name__icontains=self.q - ) + q_parts = self.q.split(" ") + q = Q() + for part in q_parts: + q &= Q(name__icontains=part) + qs = qs.filter(q) qs = qs.order_by( "name" ) From f49bb74c38275a7ce687778455c9840afdeb1145 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 25 Aug 2022 11:34:09 +0200 Subject: [PATCH 32/48] Login required on modals * adds new login_required_modal decorator * can be used before regular login_required decorator to return a proper session-timed-out message --- compensation/views/compensation/action.py | 5 +- .../views/compensation/compensation.py | 3 +- compensation/views/compensation/deadline.py | 5 +- compensation/views/compensation/document.py | 5 +- compensation/views/compensation/log.py | 3 +- .../views/compensation/resubmission.py | 3 +- compensation/views/compensation/state.py | 5 +- compensation/views/eco_account/action.py | 5 +- compensation/views/eco_account/deadline.py | 5 +- compensation/views/eco_account/deduction.py | 5 +- compensation/views/eco_account/document.py | 7 +- compensation/views/eco_account/eco_account.py | 6 +- compensation/views/eco_account/log.py | 3 +- compensation/views/eco_account/record.py | 3 +- .../views/eco_account/resubmission.py | 3 +- compensation/views/eco_account/share.py | 3 +- compensation/views/eco_account/state.py | 5 +- ema/views/action.py | 5 +- ema/views/deadline.py | 5 +- ema/views/document.py | 5 +- ema/views/ema.py | 3 +- ema/views/log.py | 3 +- ema/views/record.py | 3 +- ema/views/resubmission.py | 3 +- ema/views/share.py | 3 +- ema/views/state.py | 5 +- intervention/views/compensation.py | 3 +- intervention/views/intervention.py | 3 +- intervention/views/resubmission.py | 3 +- intervention/views/revocation.py | 3 +- intervention/views/share.py | 3 +- konova/decorators.py | 30 +- locale/de/LC_MESSAGES/django.mo | Bin 45149 -> 45396 bytes locale/de/LC_MESSAGES/django.po | 1219 +++++++++-------- templates/modal/modal_session_timed_out.html | 3 + user/views.py | 8 +- 36 files changed, 761 insertions(+), 623 deletions(-) create mode 100644 templates/modal/modal_session_timed_out.html diff --git a/compensation/views/compensation/action.py b/compensation/views/compensation/action.py index 1f5b9339..aa87c71d 100644 --- a/compensation/views/compensation/action.py +++ b/compensation/views/compensation/action.py @@ -14,7 +14,7 @@ from django.utils.decorators import method_decorator from compensation.forms.modals.compensation_action import RemoveCompensationActionModalForm, \ EditCompensationActionModalForm, NewCompensationActionModalForm from compensation.models import Compensation, CompensationAction -from konova.decorators import shared_access_required, default_group_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_EDITED, \ COMPENSATION_ACTION_ADDED from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ @@ -25,6 +25,7 @@ class NewCompensationActionView(AbstractNewCompensationActionView): model = Compensation redirect_url = "compensation:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) @@ -36,6 +37,7 @@ class EditCompensationActionView(AbstractEditCompensationActionView): model = Compensation redirect_url = "compensation:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) @@ -47,6 +49,7 @@ class RemoveCompensationActionView(AbstractRemoveCompensationActionView): model = Compensation redirect_url = "compensation:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index dd461100..57646895 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -19,7 +19,7 @@ from compensation.models import Compensation from compensation.tables.compensation import CompensationTable from intervention.models import Intervention from konova.contexts import BaseContext -from konova.decorators import shared_access_required, default_group_required, any_group_check +from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal from konova.forms import SimpleGeomForm from konova.forms.modals import RemoveModalForm from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP @@ -249,6 +249,7 @@ def detail_view(request: HttpRequest, id: str): return render(request, template, context) +@login_required_modal @login_required @default_group_required @shared_access_required(Compensation, "id") diff --git a/compensation/views/compensation/deadline.py b/compensation/views/compensation/deadline.py index 7463b7ec..7e2a9fb3 100644 --- a/compensation/views/compensation/deadline.py +++ b/compensation/views/compensation/deadline.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from compensation.models import Compensation -from konova.decorators import shared_access_required, default_group_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.deadline import AbstractRemoveDeadlineView, AbstractEditDeadlineView, AbstractNewDeadlineView @@ -17,6 +17,7 @@ class NewCompensationDeadlineView(AbstractNewDeadlineView): model = Compensation redirect_url = "compensation:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) @@ -28,6 +29,7 @@ class EditCompensationDeadlineView(AbstractEditDeadlineView): model = Compensation redirect_url = "compensation:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) @@ -39,6 +41,7 @@ class RemoveCompensationDeadlineView(AbstractRemoveDeadlineView): model = Compensation redirect_url = "compensation:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) diff --git a/compensation/views/compensation/document.py b/compensation/views/compensation/document.py index cb31739e..cb7de2a8 100644 --- a/compensation/views/compensation/document.py +++ b/compensation/views/compensation/document.py @@ -10,7 +10,7 @@ from django.utils.decorators import method_decorator from compensation.forms.modals.document import NewCompensationDocumentModalForm from compensation.models import Compensation, CompensationDocument -from konova.decorators import shared_access_required, default_group_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.forms.modals import EditDocumentModalForm from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ AbstractEditDocumentView @@ -21,6 +21,7 @@ class NewCompensationDocumentView(AbstractNewDocumentView): form = NewCompensationDocumentModalForm redirect_url = "compensation:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) @@ -43,6 +44,7 @@ class RemoveCompensationDocumentView(AbstractRemoveDocumentView): model = Compensation document_model = CompensationDocument + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) @@ -56,6 +58,7 @@ class EditCompensationDocumentView(AbstractEditDocumentView): form = EditDocumentModalForm redirect_url = "compensation:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) diff --git a/compensation/views/compensation/log.py b/compensation/views/compensation/log.py index 22406843..40b15a1e 100644 --- a/compensation/views/compensation/log.py +++ b/compensation/views/compensation/log.py @@ -9,13 +9,14 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from compensation.models import Compensation -from konova.decorators import shared_access_required, default_group_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.log import AbstractLogView class CompensationLogView(AbstractLogView): model = Compensation + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) diff --git a/compensation/views/compensation/resubmission.py b/compensation/views/compensation/resubmission.py index dfcbad20..31b073e7 100644 --- a/compensation/views/compensation/resubmission.py +++ b/compensation/views/compensation/resubmission.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from compensation.models import Compensation -from konova.decorators import shared_access_required, default_group_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.resubmission import AbstractResubmissionView @@ -18,6 +18,7 @@ class CompensationResubmissionView(AbstractResubmissionView): redirect_url_base = "compensation:detail" form_action_url_base = "compensation:resubmission-create" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) diff --git a/compensation/views/compensation/state.py b/compensation/views/compensation/state.py index 136e8604..8fffbbd7 100644 --- a/compensation/views/compensation/state.py +++ b/compensation/views/compensation/state.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from compensation.models import Compensation -from konova.decorators import shared_access_required, default_group_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ AbstractRemoveCompensationStateView @@ -18,6 +18,7 @@ class NewCompensationStateView(AbstractNewCompensationStateView): model = Compensation redirect_url = "compensation:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) @@ -29,6 +30,7 @@ class EditCompensationStateView(AbstractEditCompensationStateView): model = Compensation redirect_url = "compensation:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) @@ -40,6 +42,7 @@ class RemoveCompensationStateView(AbstractRemoveCompensationStateView): model = Compensation redirect_url = "compensation:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Compensation, "id")) diff --git a/compensation/views/eco_account/action.py b/compensation/views/eco_account/action.py index e459def7..6aca3825 100644 --- a/compensation/views/eco_account/action.py +++ b/compensation/views/eco_account/action.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from compensation.models import EcoAccount -from konova.decorators import shared_access_required, default_group_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ AbstractRemoveCompensationActionView @@ -18,6 +18,7 @@ class NewEcoAccountActionView(AbstractNewCompensationActionView): model = EcoAccount redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) @@ -29,6 +30,7 @@ class EditEcoAccountActionView(AbstractEditCompensationActionView): model = EcoAccount redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) @@ -40,6 +42,7 @@ class RemoveEcoAccountActionView(AbstractRemoveCompensationActionView): model = EcoAccount redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) diff --git a/compensation/views/eco_account/deadline.py b/compensation/views/eco_account/deadline.py index 15b4aa8c..c49dba35 100644 --- a/compensation/views/eco_account/deadline.py +++ b/compensation/views/eco_account/deadline.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from compensation.models import EcoAccount -from konova.decorators import shared_access_required, default_group_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.deadline import AbstractNewDeadlineView, AbstractEditDeadlineView, AbstractRemoveDeadlineView @@ -17,6 +17,7 @@ class NewEcoAccountDeadlineView(AbstractNewDeadlineView): model = EcoAccount redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) @@ -28,6 +29,7 @@ class EditEcoAccountDeadlineView(AbstractEditDeadlineView): model = EcoAccount redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) @@ -39,6 +41,7 @@ class RemoveEcoAccountDeadlineView(AbstractRemoveDeadlineView): model = EcoAccount redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) diff --git a/compensation/views/eco_account/deduction.py b/compensation/views/eco_account/deduction.py index c7598bb8..1de6c605 100644 --- a/compensation/views/eco_account/deduction.py +++ b/compensation/views/eco_account/deduction.py @@ -10,7 +10,7 @@ from django.http import Http404 from django.utils.decorators import method_decorator from compensation.models import EcoAccount -from konova.decorators import default_group_required +from konova.decorators import default_group_required, login_required_modal from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView @@ -18,6 +18,7 @@ class NewEcoAccountDeductionView(AbstractNewDeductionView): model = EcoAccount redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) def dispatch(self, request, *args, **kwargs): @@ -35,6 +36,7 @@ class EditEcoAccountDeductionView(AbstractEditDeductionView): model = EcoAccount redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) def dispatch(self, request, *args, **kwargs): @@ -48,6 +50,7 @@ class RemoveEcoAccountDeductionView(AbstractRemoveDeductionView): model = EcoAccount redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) def dispatch(self, request, *args, **kwargs): diff --git a/compensation/views/eco_account/document.py b/compensation/views/eco_account/document.py index 40b40a04..73fdcd44 100644 --- a/compensation/views/eco_account/document.py +++ b/compensation/views/eco_account/document.py @@ -13,10 +13,8 @@ from django.utils.decorators import method_decorator from compensation.forms.modals.document import NewEcoAccountDocumentModalForm from compensation.models import EcoAccount, EcoAccountDocument -from konova.decorators import shared_access_required, default_group_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.forms.modals import EditDocumentModalForm -from konova.utils.documents import remove_document, get_document -from konova.utils.message_templates import DOCUMENT_EDITED, DOCUMENT_ADDED from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ AbstractEditDocumentView @@ -26,6 +24,7 @@ class NewEcoAccountDocumentView(AbstractNewDocumentView): form = NewEcoAccountDocumentModalForm redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) @@ -48,6 +47,7 @@ class RemoveEcoAccountDocumentView(AbstractRemoveDocumentView): model = EcoAccount document_model = EcoAccountDocument + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) @@ -61,6 +61,7 @@ class EditEcoAccountDocumentView(AbstractEditDocumentView): form = EditDocumentModalForm redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index df99b957..f75fcc18 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -17,12 +17,13 @@ from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm from compensation.models import EcoAccount from compensation.tables.eco_account import EcoAccountTable from konova.contexts import BaseContext -from konova.decorators import shared_access_required, default_group_required, any_group_check +from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal from konova.forms import SimpleGeomForm from konova.forms.modals import RemoveModalForm from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID +from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \ + IDENTIFIER_REPLACED from konova.utils.user_checks import in_group @@ -234,6 +235,7 @@ def detail_view(request: HttpRequest, id: str): return render(request, template, context) +@login_required_modal @login_required @default_group_required @shared_access_required(EcoAccount, "id") diff --git a/compensation/views/eco_account/log.py b/compensation/views/eco_account/log.py index b9ca5bc3..e18d945a 100644 --- a/compensation/views/eco_account/log.py +++ b/compensation/views/eco_account/log.py @@ -9,13 +9,14 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from compensation.models import EcoAccount -from konova.decorators import shared_access_required, default_group_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.log import AbstractLogView class EcoAccountLogView(AbstractLogView): model = EcoAccount + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) diff --git a/compensation/views/eco_account/record.py b/compensation/views/eco_account/record.py index 18ccf237..0d1f2070 100644 --- a/compensation/views/eco_account/record.py +++ b/compensation/views/eco_account/record.py @@ -9,13 +9,14 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from compensation.models import EcoAccount -from konova.decorators import shared_access_required, conservation_office_group_required +from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.views.record import AbstractRecordView class EcoAccountRecordView(AbstractRecordView): model = EcoAccount + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) diff --git a/compensation/views/eco_account/resubmission.py b/compensation/views/eco_account/resubmission.py index afe8c713..19b8dca4 100644 --- a/compensation/views/eco_account/resubmission.py +++ b/compensation/views/eco_account/resubmission.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from compensation.models import EcoAccount -from konova.decorators import shared_access_required, default_group_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.resubmission import AbstractResubmissionView @@ -18,6 +18,7 @@ class EcoAccountResubmissionView(AbstractResubmissionView): redirect_url_base = "compensation:acc:detail" form_action_url_base = "compensation:acc:resubmission-create" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) diff --git a/compensation/views/eco_account/share.py b/compensation/views/eco_account/share.py index c2f2e53f..19c8903a 100644 --- a/compensation/views/eco_account/share.py +++ b/compensation/views/eco_account/share.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from compensation.models import EcoAccount -from konova.decorators import shared_access_required, default_group_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.share import AbstractShareByTokenView, AbstractShareFormView @@ -25,6 +25,7 @@ class EcoAccountShareByTokenView(AbstractShareByTokenView): class EcoAccountShareFormView(AbstractShareFormView): model = EcoAccount + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) diff --git a/compensation/views/eco_account/state.py b/compensation/views/eco_account/state.py index 0591a17c..1a28491a 100644 --- a/compensation/views/eco_account/state.py +++ b/compensation/views/eco_account/state.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from compensation.models import EcoAccount -from konova.decorators import shared_access_required, default_group_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ AbstractRemoveCompensationStateView @@ -18,6 +18,7 @@ class NewEcoAccountStateView(AbstractNewCompensationStateView): model = EcoAccount redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) @@ -29,6 +30,7 @@ class EditEcoAccountStateView(AbstractEditCompensationStateView): model = EcoAccount redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) @@ -40,6 +42,7 @@ class RemoveEcoAccountStateView(AbstractRemoveCompensationStateView): model = EcoAccount redirect_url = "compensation:acc:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(EcoAccount, "id")) diff --git a/ema/views/action.py b/ema/views/action.py index bae950c6..068c224a 100644 --- a/ema/views/action.py +++ b/ema/views/action.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from ema.models import Ema -from konova.decorators import shared_access_required, conservation_office_group_required +from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ AbstractRemoveCompensationActionView @@ -18,6 +18,7 @@ class NewEmaActionView(AbstractNewCompensationActionView): model = Ema redirect_url = "ema:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) @@ -29,6 +30,7 @@ class EditEmaActionView(AbstractEditCompensationActionView): model = Ema redirect_url = "ema:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) @@ -40,6 +42,7 @@ class RemoveEmaActionView(AbstractRemoveCompensationActionView): model = Ema redirect_url = "ema:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) diff --git a/ema/views/deadline.py b/ema/views/deadline.py index 76c2bded..d760bdab 100644 --- a/ema/views/deadline.py +++ b/ema/views/deadline.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from ema.models import Ema -from konova.decorators import shared_access_required, conservation_office_group_required +from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.views.deadline import AbstractNewDeadlineView, AbstractRemoveDeadlineView, AbstractEditDeadlineView @@ -17,6 +17,7 @@ class NewEmaDeadlineView(AbstractNewDeadlineView): model = Ema redirect_url = "ema:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) @@ -28,6 +29,7 @@ class EditEmaDeadlineView(AbstractEditDeadlineView): model = Ema redirect_url = "ema:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) @@ -39,6 +41,7 @@ class RemoveEmaDeadlineView(AbstractRemoveDeadlineView): model = Ema redirect_url = "ema:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) diff --git a/ema/views/document.py b/ema/views/document.py index d394c0fe..45c58146 100644 --- a/ema/views/document.py +++ b/ema/views/document.py @@ -10,7 +10,7 @@ from django.utils.decorators import method_decorator from ema.forms import NewEmaDocumentModalForm from ema.models import Ema, EmaDocument -from konova.decorators import shared_access_required, conservation_office_group_required +from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.forms.modals import EditDocumentModalForm from konova.views.document import AbstractEditDocumentView, AbstractRemoveDocumentView, AbstractGetDocumentView, \ AbstractNewDocumentView @@ -21,6 +21,7 @@ class NewEmaDocumentView(AbstractNewDocumentView): form = NewEmaDocumentModalForm redirect_url = "ema:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) @@ -43,6 +44,7 @@ class RemoveEmaDocumentView(AbstractRemoveDocumentView): model = Ema document_model = EmaDocument + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) @@ -56,6 +58,7 @@ class EditEmaDocumentView(AbstractEditDocumentView): form = EditDocumentModalForm redirect_url = "ema:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) diff --git a/ema/views/ema.py b/ema/views/ema.py index f2b4d3b8..b298959f 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -17,7 +17,7 @@ from ema.forms import NewEmaForm, EditEmaForm from ema.models import Ema from ema.tables import EmaTable from konova.contexts import BaseContext -from konova.decorators import shared_access_required, conservation_office_group_required +from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.forms import SimpleGeomForm from konova.forms.modals import RemoveModalForm from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP @@ -214,6 +214,7 @@ def edit_view(request: HttpRequest, id: str): return render(request, template, context) +@login_required_modal @login_required @conservation_office_group_required @shared_access_required(Ema, "id") diff --git a/ema/views/log.py b/ema/views/log.py index 80b4a9ff..82162ba4 100644 --- a/ema/views/log.py +++ b/ema/views/log.py @@ -9,13 +9,14 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from ema.models import Ema -from konova.decorators import shared_access_required, conservation_office_group_required +from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.views.log import AbstractLogView class EmaLogView(AbstractLogView): model = Ema + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) diff --git a/ema/views/record.py b/ema/views/record.py index 7dfc946b..83560999 100644 --- a/ema/views/record.py +++ b/ema/views/record.py @@ -9,13 +9,14 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from ema.models import Ema -from konova.decorators import shared_access_required, conservation_office_group_required +from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.views.record import AbstractRecordView class EmaRecordView(AbstractRecordView): model = Ema + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) diff --git a/ema/views/resubmission.py b/ema/views/resubmission.py index 384c3cde..c07c79ee 100644 --- a/ema/views/resubmission.py +++ b/ema/views/resubmission.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from ema.models import Ema -from konova.decorators import shared_access_required, conservation_office_group_required +from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.views.resubmission import AbstractResubmissionView @@ -18,6 +18,7 @@ class EmaResubmissionView(AbstractResubmissionView): redirect_url_base = "ema:detail" form_action_url_base = "ema:resubmission-create" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) diff --git a/ema/views/share.py b/ema/views/share.py index 536fe31e..00b75e7c 100644 --- a/ema/views/share.py +++ b/ema/views/share.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from ema.models import Ema -from konova.decorators import conservation_office_group_required, shared_access_required +from konova.decorators import conservation_office_group_required, shared_access_required, login_required_modal from konova.views.share import AbstractShareByTokenView, AbstractShareFormView @@ -25,6 +25,7 @@ class EmaShareByTokenView(AbstractShareByTokenView): class EmaShareFormView(AbstractShareFormView): model = Ema + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) diff --git a/ema/views/state.py b/ema/views/state.py index 717ae7f7..e8e489dc 100644 --- a/ema/views/state.py +++ b/ema/views/state.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from ema.models import Ema -from konova.decorators import conservation_office_group_required, shared_access_required +from konova.decorators import conservation_office_group_required, shared_access_required, login_required_modal from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ AbstractRemoveCompensationStateView @@ -18,6 +18,7 @@ class NewEmaStateView(AbstractNewCompensationStateView): model = Ema redirect_url = "ema:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) @@ -29,6 +30,7 @@ class EditEmaStateView(AbstractEditCompensationStateView): model = Ema redirect_url = "ema:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) @@ -40,6 +42,7 @@ class RemoveEmaStateView(AbstractRemoveCompensationStateView): model = Ema redirect_url = "ema:detail" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) diff --git a/intervention/views/compensation.py b/intervention/views/compensation.py index 94e8dfd5..704a04e4 100644 --- a/intervention/views/compensation.py +++ b/intervention/views/compensation.py @@ -12,11 +12,12 @@ from django.shortcuts import get_object_or_404 from django.urls import reverse from intervention.models import Intervention -from konova.decorators import shared_access_required +from konova.decorators import shared_access_required, login_required_modal from konova.forms.modals import RemoveModalForm from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE +@login_required_modal @login_required @shared_access_required(Intervention, "id") def remove_compensation_view(request: HttpRequest, id: str, comp_id: str): diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index bf33bfb9..524c2c75 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -16,7 +16,7 @@ from intervention.forms.intervention import EditInterventionForm, NewInterventio from intervention.models import Intervention from intervention.tables import InterventionTable from konova.contexts import BaseContext -from konova.decorators import default_group_required, shared_access_required, any_group_check +from konova.decorators import default_group_required, shared_access_required, any_group_check, login_required_modal from konova.forms import SimpleGeomForm from konova.forms.modals import RemoveModalForm from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP @@ -228,6 +228,7 @@ def edit_view(request: HttpRequest, id: str): return render(request, template, context) +@login_required_modal @login_required @default_group_required @shared_access_required(Intervention, "id") diff --git a/intervention/views/resubmission.py b/intervention/views/resubmission.py index 452a3e38..37fbd632 100644 --- a/intervention/views/resubmission.py +++ b/intervention/views/resubmission.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from intervention.models import Intervention -from konova.decorators import default_group_required, shared_access_required +from konova.decorators import default_group_required, shared_access_required, login_required_modal from konova.views.resubmission import AbstractResubmissionView @@ -18,6 +18,7 @@ class InterventionResubmissionView(AbstractResubmissionView): redirect_url_base = "intervention:detail" form_action_url_base = "intervention:resubmission-create" + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Intervention, "id")) diff --git a/intervention/views/revocation.py b/intervention/views/revocation.py index 6c6c70b7..db1dd1d2 100644 --- a/intervention/views/revocation.py +++ b/intervention/views/revocation.py @@ -14,7 +14,7 @@ from django.urls import reverse from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \ RemoveRevocationModalForm from intervention.models import Intervention, RevocationDocument, Revocation -from konova.decorators import default_group_required, shared_access_required +from konova.decorators import default_group_required, shared_access_required, login_required_modal from konova.utils.documents import get_document from konova.utils.message_templates import REVOCATION_ADDED, DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED @@ -91,6 +91,7 @@ def edit_revocation_view(request: HttpRequest, id: str, revocation_id: str): ) +@login_required_modal @login_required @default_group_required @shared_access_required(Intervention, "id") diff --git a/intervention/views/share.py b/intervention/views/share.py index f78d65b2..c72e2183 100644 --- a/intervention/views/share.py +++ b/intervention/views/share.py @@ -9,7 +9,7 @@ from django.contrib.auth.decorators import login_required from django.utils.decorators import method_decorator from intervention.models import Intervention -from konova.decorators import default_group_required, shared_access_required +from konova.decorators import default_group_required, shared_access_required, login_required_modal from konova.views.share import AbstractShareByTokenView, AbstractShareFormView @@ -25,6 +25,7 @@ class InterventionShareByTokenView(AbstractShareByTokenView): class InterventionShareFormView(AbstractShareFormView): model = Intervention + @method_decorator(login_required_modal) @method_decorator(login_required) @method_decorator(default_group_required) @method_decorator(shared_access_required(Intervention, "id")) diff --git a/konova/decorators.py b/konova/decorators.py index 583afbee..cbf45dc4 100644 --- a/konova/decorators.py +++ b/konova/decorators.py @@ -8,12 +8,12 @@ Created on: 16.11.20 from functools import wraps +from bootstrap_modal_forms.utils import is_ajax from django.contrib import messages -from django.shortcuts import redirect, get_object_or_404 +from django.shortcuts import redirect, get_object_or_404, render from django.urls import reverse from django.utils.translation import gettext_lazy as _ -from konova.settings import DEFAULT_GROUP, ETS_GROUP, ZB_GROUP from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED @@ -146,4 +146,28 @@ def shared_access_required(obj_class, id_key): return redirect("home") return function(request, *args, **kwargs) return wrap - return decorator \ No newline at end of file + return decorator + + +def login_required_modal(function): + """ Checks on modal requests whether the user is authenticated or not + + If not, the user will not be redirected but informed about the need to relogin. + + """ + @wraps(function) + def wrap(request, *args, **kwargs): + is_modal_request = is_ajax(request.META) + is_user_not_logged_in = not request.user.is_authenticated + + if is_modal_request and is_user_not_logged_in: + template = "modal/modal_generic.html" + body_template = "modal/modal_session_timed_out.html" + + context = { + "modal_body_template": body_template, + "modal_title": _("Session timed out"), + } + return render(request, template, context) + return function(request, *args, **kwargs) + return wrap diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 03853f485e18ce219bb50fbd9b452d147282bbb3..fadbaa83a217916800a369beee95f18180e62dba 100644 GIT binary patch delta 12535 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 delta 12218 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 c2885, YEAR. # -#: compensation/filters.py:123 compensation/forms/modalForms.py:37 -#: compensation/forms/modalForms.py:48 compensation/forms/modalForms.py:64 -#: compensation/forms/modalForms.py:362 compensation/forms/modalForms.py:470 -#: intervention/forms/forms.py:54 intervention/forms/forms.py:174 -#: intervention/forms/forms.py:186 intervention/forms/modalForms.py:150 -#: intervention/forms/modalForms.py:163 intervention/forms/modalForms.py:176 -#: konova/filters/mixins.py:53 konova/filters/mixins.py:54 -#: konova/filters/mixins.py:81 konova/filters/mixins.py:82 -#: konova/filters/mixins.py:94 konova/filters/mixins.py:95 -#: konova/filters/mixins.py:107 konova/filters/mixins.py:108 -#: konova/filters/mixins.py:120 konova/filters/mixins.py:121 -#: konova/filters/mixins.py:134 konova/filters/mixins.py:135 -#: konova/filters/mixins.py:277 konova/filters/mixins.py:323 -#: konova/filters/mixins.py:361 konova/filters/mixins.py:362 -#: konova/filters/mixins.py:393 konova/filters/mixins.py:394 -#: konova/forms.py:183 konova/forms.py:285 konova/forms.py:399 -#: konova/forms.py:443 konova/forms.py:453 konova/forms.py:466 -#: konova/forms.py:478 konova/forms.py:496 konova/forms.py:696 -#: konova/forms.py:711 user/forms.py:42 +#: compensation/filters/eco_account.py:21 +#: compensation/forms/modals/compensation_action.py:82 +#: compensation/forms/modals/deadline.py:50 +#: compensation/forms/modals/payment.py:23 +#: compensation/forms/modals/payment.py:34 +#: compensation/forms/modals/payment.py:50 +#: intervention/forms/intervention.py:55 intervention/forms/intervention.py:175 +#: intervention/forms/intervention.py:187 +#: intervention/forms/modals/revocation.py:20 +#: intervention/forms/modals/revocation.py:33 +#: intervention/forms/modals/revocation.py:46 +#: konova/filters/mixins/file_number.py:17 +#: konova/filters/mixins/file_number.py:18 +#: konova/filters/mixins/geo_reference.py:25 +#: konova/filters/mixins/geo_reference.py:26 +#: konova/filters/mixins/geo_reference.py:38 +#: konova/filters/mixins/geo_reference.py:39 +#: konova/filters/mixins/geo_reference.py:51 +#: konova/filters/mixins/geo_reference.py:52 +#: konova/filters/mixins/geo_reference.py:64 +#: konova/filters/mixins/geo_reference.py:65 +#: konova/filters/mixins/geo_reference.py:78 +#: konova/filters/mixins/geo_reference.py:79 konova/filters/mixins/office.py:24 +#: konova/filters/mixins/office.py:25 konova/filters/mixins/office.py:56 +#: konova/filters/mixins/office.py:57 konova/filters/mixins/record.py:23 +#: konova/filters/mixins/share.py:23 konova/forms/geometry_form.py:30 +#: konova/forms/modals/document_form.py:25 +#: konova/forms/modals/document_form.py:35 +#: konova/forms/modals/document_form.py:48 +#: konova/forms/modals/document_form.py:60 +#: konova/forms/modals/document_form.py:78 +#: konova/forms/modals/remove_form.py:23 +#: konova/forms/modals/resubmission_form.py:21 +#: konova/forms/modals/resubmission_form.py:36 konova/forms/remove_form.py:19 +#: user/forms/user.py:39 #, fuzzy msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2022-08-15 09:39+0200\n" +"POT-Creation-Date: 2022-08-25 10:57+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -53,28 +69,29 @@ msgstr "Bis" msgid "Entries created until..." msgstr "Einträge erstellt bis..." -#: analysis/forms.py:49 compensation/forms/forms.py:77 +#: analysis/forms.py:49 compensation/forms/mixins.py:21 #: compensation/templates/compensation/detail/eco_account/view.html:59 #: compensation/templates/compensation/report/eco_account/report.html:16 #: compensation/utils/quality.py:111 ema/templates/ema/detail/view.html:49 #: ema/templates/ema/report/report.html:16 ema/utils/quality.py:26 -#: intervention/forms/forms.py:102 +#: intervention/forms/intervention.py:103 #: intervention/templates/intervention/detail/view.html:56 #: intervention/templates/intervention/report/report.html:37 -#: intervention/utils/quality.py:49 konova/filters/mixins.py:403 +#: intervention/utils/quality.py:49 konova/filters/mixins/office.py:34 msgid "Conservation office" msgstr "Eintragungsstelle" -#: analysis/forms.py:51 compensation/forms/forms.py:79 +#: analysis/forms.py:51 compensation/forms/mixins.py:23 msgid "Select the responsible office" msgstr "Verantwortliche Stelle" -#: analysis/forms.py:60 compensation/forms/forms.py:88 -#: compensation/forms/forms.py:118 compensation/forms/forms.py:199 -#: intervention/forms/forms.py:64 intervention/forms/forms.py:81 -#: intervention/forms/forms.py:97 intervention/forms/forms.py:113 -#: intervention/forms/forms.py:154 intervention/forms/modalForms.py:49 -#: intervention/forms/modalForms.py:63 user/forms.py:196 user/forms.py:260 +#: analysis/forms.py:60 compensation/forms/compensation.py:93 +#: compensation/forms/mixins.py:32 compensation/forms/mixins.py:62 +#: intervention/forms/intervention.py:65 intervention/forms/intervention.py:82 +#: intervention/forms/intervention.py:98 intervention/forms/intervention.py:114 +#: intervention/forms/intervention.py:155 intervention/forms/modals/share.py:41 +#: intervention/forms/modals/share.py:55 user/forms/modals/team.py:48 +#: user/forms/modals/team.py:112 msgid "Click for selection" msgstr "Auswählen..." @@ -86,7 +103,7 @@ msgstr "Bericht generieren" msgid "Select a timespan and the desired conservation office" msgstr "Wählen Sie die Zeitspanne und die gewünschte Eintragungsstelle" -#: analysis/forms.py:71 konova/forms.py:231 +#: analysis/forms.py:71 konova/forms/modals/base_form.py:30 msgid "Continue" msgstr "Weiter" @@ -106,7 +123,7 @@ msgstr "" #: analysis/templates/analysis/reports/includes/eco_account/amount.html:3 #: analysis/templates/analysis/reports/includes/intervention/amount.html:3 #: analysis/templates/analysis/reports/includes/old_data/amount.html:3 -#: compensation/forms/modalForms.py:454 +#: compensation/forms/modals/compensation_action.py:66 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:34 #: intervention/templates/intervention/detail/includes/deductions.html:31 msgid "Amount" @@ -167,7 +184,7 @@ msgstr "Einzelflächen" #: analysis/templates/analysis/reports/includes/intervention/amount.html:18 #: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:9 #: analysis/templates/analysis/reports/includes/intervention/laws.html:20 -#: compensation/tables.py:38 +#: compensation/tables/compensation.py:38 #: compensation/templates/compensation/detail/compensation/view.html:74 #: intervention/tables.py:38 #: intervention/templates/intervention/detail/view.html:68 @@ -183,7 +200,7 @@ msgstr "Geprüft" #: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:10 #: analysis/templates/analysis/reports/includes/intervention/laws.html:23 #: analysis/templates/analysis/reports/includes/old_data/amount.html:19 -#: compensation/tables.py:44 compensation/tables.py:219 +#: compensation/tables/compensation.py:44 compensation/tables/eco_account.py:48 #: compensation/templates/compensation/detail/compensation/view.html:93 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:31 #: compensation/templates/compensation/detail/eco_account/view.html:45 @@ -207,7 +224,7 @@ msgid "Other registration office" msgstr "Andere Zulassungsbehörden" #: analysis/templates/analysis/reports/includes/compensation/card_compensation.html:11 -#: compensation/tables.py:65 +#: compensation/tables/compensation.py:65 #: intervention/templates/intervention/detail/includes/compensations.html:8 #: intervention/templates/intervention/report/report.html:45 msgid "Compensations" @@ -234,14 +251,14 @@ msgstr "" #: analysis/templates/analysis/reports/includes/eco_account/deductions.html:14 #: analysis/templates/analysis/reports/includes/eco_account/deductions.html:16 -#: compensation/forms/modalForms.py:187 +#: compensation/forms/modals/state.py:58 #: compensation/templates/compensation/detail/compensation/includes/states-after.html:36 #: compensation/templates/compensation/detail/compensation/includes/states-before.html:36 #: compensation/templates/compensation/detail/eco_account/includes/states-after.html:36 #: compensation/templates/compensation/detail/eco_account/includes/states-before.html:36 #: ema/templates/ema/detail/includes/states-after.html:36 #: ema/templates/ema/detail/includes/states-before.html:36 -#: intervention/forms/modalForms.py:364 +#: intervention/forms/modals/deduction.py:47 #: templates/email/other/deduction_changed.html:31 #: templates/email/other/deduction_changed_team.html:31 msgid "Surface" @@ -269,7 +286,7 @@ msgid "Compensation" msgstr "Kompensation" #: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:21 -#: compensation/forms/modalForms.py:77 +#: compensation/forms/modals/payment.py:63 msgid "Payment" msgstr "Zahlung" @@ -292,7 +309,7 @@ msgstr "" " " #: analysis/templates/analysis/reports/includes/intervention/laws.html:14 -#: intervention/forms/forms.py:69 +#: intervention/forms/intervention.py:70 #: intervention/templates/intervention/detail/view.html:39 #: intervention/templates/intervention/report/report.html:20 msgid "Law" @@ -306,8 +323,9 @@ msgid "Type" msgstr "Typ" #: analysis/templates/analysis/reports/includes/old_data/amount.html:24 -#: compensation/tables.py:87 intervention/forms/modalForms.py:375 -#: intervention/forms/modalForms.py:382 intervention/tables.py:87 +#: compensation/tables/compensation.py:87 +#: intervention/forms/modals/deduction.py:58 +#: intervention/forms/modals/deduction.py:65 intervention/tables.py:87 #: intervention/templates/intervention/detail/view.html:19 #: konova/templates/konova/includes/quickstart/interventions.html:4 #: templates/email/other/deduction_changed.html:26 @@ -317,9 +335,10 @@ msgid "Intervention" msgstr "Eingriff" #: analysis/templates/analysis/reports/includes/old_data/amount.html:34 -#: compensation/tables.py:263 +#: compensation/tables/eco_account.py:92 #: compensation/templates/compensation/detail/eco_account/view.html:20 -#: intervention/forms/modalForms.py:348 intervention/forms/modalForms.py:355 +#: intervention/forms/modals/deduction.py:31 +#: intervention/forms/modals/deduction.py:38 #: konova/templates/konova/includes/quickstart/ecoaccounts.html:4 #: templates/navbars/navbar.html:34 msgid "Eco-account" @@ -333,24 +352,24 @@ msgstr "Altfälle" msgid "Binding date before" msgstr "Bestandskraft- bzw. Rechtskraftdatum vor" -#: compensation/filters.py:122 +#: compensation/filters/eco_account.py:20 msgid "Show only unrecorded" msgstr "Nur unverzeichnete anzeigen" -#: compensation/forms/forms.py:32 compensation/tables.py:23 -#: compensation/tables.py:194 ema/tables.py:29 intervention/forms/forms.py:28 -#: intervention/tables.py:23 +#: compensation/forms/compensation.py:30 compensation/tables/compensation.py:23 +#: compensation/tables/eco_account.py:23 ema/tables.py:29 +#: intervention/forms/intervention.py:29 intervention/tables.py:23 #: intervention/templates/intervention/detail/includes/compensations.html:30 msgid "Identifier" msgstr "Kennung" -#: compensation/forms/forms.py:35 intervention/forms/forms.py:31 -#: user/forms.py:126 +#: compensation/forms/compensation.py:33 intervention/forms/intervention.py:32 +#: user/forms/user.py:77 msgid "Generated automatically" msgstr "Automatisch generiert" -#: compensation/forms/forms.py:44 compensation/tables.py:28 -#: compensation/tables.py:199 +#: compensation/forms/compensation.py:42 compensation/tables/compensation.py:28 +#: compensation/tables/eco_account.py:28 #: compensation/templates/compensation/detail/compensation/includes/documents.html:28 #: compensation/templates/compensation/detail/compensation/view.html:32 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:28 @@ -359,26 +378,28 @@ msgstr "Automatisch generiert" #: compensation/templates/compensation/report/eco_account/report.html:12 #: ema/tables.py:34 ema/templates/ema/detail/includes/documents.html:28 #: ema/templates/ema/detail/view.html:31 -#: ema/templates/ema/report/report.html:12 intervention/forms/forms.py:40 -#: intervention/tables.py:28 +#: ema/templates/ema/report/report.html:12 +#: intervention/forms/intervention.py:41 intervention/tables.py:28 #: intervention/templates/intervention/detail/includes/compensations.html:33 #: intervention/templates/intervention/detail/includes/documents.html:28 #: intervention/templates/intervention/detail/view.html:31 #: intervention/templates/intervention/report/report.html:12 -#: konova/forms.py:442 +#: konova/forms/modals/document_form.py:24 msgid "Title" msgstr "Bezeichnung" -#: compensation/forms/forms.py:46 intervention/forms/forms.py:42 +#: compensation/forms/compensation.py:44 intervention/forms/intervention.py:43 msgid "An explanatory name" msgstr "Aussagekräftiger Titel" -#: compensation/forms/forms.py:50 ema/forms.py:52 ema/forms.py:112 +#: compensation/forms/compensation.py:48 ema/forms.py:51 ema/forms.py:111 msgid "Compensation XY; Location ABC" msgstr "Kompensation XY; Flur ABC" -#: compensation/forms/forms.py:57 compensation/forms/modalForms.py:63 -#: compensation/forms/modalForms.py:361 compensation/forms/modalForms.py:469 +#: compensation/forms/compensation.py:55 +#: compensation/forms/modals/compensation_action.py:81 +#: compensation/forms/modals/deadline.py:49 +#: compensation/forms/modals/payment.py:49 #: compensation/templates/compensation/detail/compensation/includes/actions.html:35 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:39 #: compensation/templates/compensation/detail/compensation/includes/documents.html:34 @@ -388,76 +409,130 @@ msgstr "Kompensation XY; Flur ABC" #: ema/templates/ema/detail/includes/actions.html:34 #: ema/templates/ema/detail/includes/deadlines.html:39 #: ema/templates/ema/detail/includes/documents.html:34 -#: intervention/forms/forms.py:198 intervention/forms/modalForms.py:175 +#: intervention/forms/intervention.py:199 +#: intervention/forms/modals/revocation.py:45 #: intervention/templates/intervention/detail/includes/documents.html:34 #: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/revocation.html:38 -#: konova/forms.py:477 konova/forms.py:710 +#: konova/forms/modals/document_form.py:59 +#: konova/forms/modals/resubmission_form.py:35 #: konova/templates/konova/includes/comment_card.html:16 msgid "Comment" msgstr "Kommentar" -#: compensation/forms/forms.py:59 compensation/forms/modalForms.py:471 -#: intervention/forms/forms.py:200 konova/forms.py:712 +#: compensation/forms/compensation.py:57 +#: compensation/forms/modals/compensation_action.py:83 +#: intervention/forms/intervention.py:201 +#: konova/forms/modals/resubmission_form.py:37 msgid "Additional comment" msgstr "Zusätzlicher Kommentar" -#: compensation/forms/forms.py:93 +#: compensation/forms/compensation.py:84 +#: compensation/templates/compensation/detail/compensation/view.html:36 +#: compensation/templates/compensation/report/compensation/report.html:16 +msgid "compensates intervention" +msgstr "kompensiert Eingriff" + +#: compensation/forms/compensation.py:86 +msgid "Select the intervention for which this compensation compensates" +msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist" + +#: compensation/forms/compensation.py:113 +#: compensation/views/compensation/compensation.py:113 +msgid "New compensation" +msgstr "Neue Kompensation" + +#: compensation/forms/compensation.py:186 +msgid "Edit compensation" +msgstr "Bearbeite Kompensation" + +#: compensation/forms/eco_account.py:29 compensation/utils/quality.py:95 +msgid "Available Surface" +msgstr "Verfügbare Fläche" + +#: compensation/forms/eco_account.py:32 +msgid "The amount that can be used for deductions" +msgstr "Die für Abbuchungen zur Verfügung stehende Menge" + +#: compensation/forms/eco_account.py:41 +#: compensation/templates/compensation/detail/eco_account/view.html:67 +#: compensation/utils/quality.py:83 +msgid "Agreement date" +msgstr "Vereinbarungsdatum" + +#: compensation/forms/eco_account.py:43 +msgid "When did the parties agree on this?" +msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?" + +#: compensation/forms/eco_account.py:69 +#: compensation/views/eco_account/eco_account.py:94 +msgid "New Eco-Account" +msgstr "Neues Ökokonto" + +#: compensation/forms/eco_account.py:78 +msgid "Eco-Account XY; Location ABC" +msgstr "Ökokonto XY; Flur ABC" + +#: compensation/forms/eco_account.py:140 +msgid "Edit Eco-Account" +msgstr "Ökokonto bearbeiten" + +#: compensation/forms/mixins.py:37 #: compensation/templates/compensation/detail/eco_account/view.html:63 #: compensation/templates/compensation/report/eco_account/report.html:20 #: compensation/utils/quality.py:113 ema/templates/ema/detail/view.html:53 #: ema/templates/ema/report/report.html:20 ema/utils/quality.py:28 -#: intervention/forms/forms.py:130 +#: intervention/forms/intervention.py:131 #: intervention/templates/intervention/detail/view.html:60 #: intervention/templates/intervention/report/report.html:41 #: intervention/utils/quality.py:42 msgid "Conservation office file number" msgstr "Aktenzeichen Eintragungsstelle" -#: compensation/forms/forms.py:99 intervention/forms/forms.py:136 +#: compensation/forms/mixins.py:43 intervention/forms/intervention.py:137 msgid "ETS-123/ABC.456" msgstr "" -#: compensation/forms/forms.py:106 +#: compensation/forms/mixins.py:50 msgid "Eco-Account handler type" msgstr "Art des Maßnahmenträgers" -#: compensation/forms/forms.py:108 +#: compensation/forms/mixins.py:52 msgid "What type of handler is responsible for the ecoaccount?" msgstr "Zu welcher Kategorie dieser Maßnahmenträger gehört" -#: compensation/forms/forms.py:123 +#: compensation/forms/mixins.py:67 msgid "Eco-Account handler detail" msgstr "Detailangabe zum Maßnahmenträger" -#: compensation/forms/forms.py:127 intervention/forms/forms.py:163 +#: compensation/forms/mixins.py:71 intervention/forms/intervention.py:164 msgid "Detail input on the handler" msgstr "Name der Behörde, Stadt, Firma, ..." -#: compensation/forms/forms.py:130 intervention/forms/forms.py:166 +#: compensation/forms/mixins.py:74 intervention/forms/intervention.py:167 msgid "Company Mustermann" msgstr "Firma Mustermann" -#: compensation/forms/forms.py:143 +#: compensation/forms/mixins.py:87 #: compensation/templates/compensation/report/compensation/report.html:34 msgid "Is CEF" msgstr "Ist CEF-Maßnahme" -#: compensation/forms/forms.py:144 +#: compensation/forms/mixins.py:88 msgid "Optionally: Whether this compensation is a CEF compensation?" msgstr "Optional: Handelt es sich um eine CEF-Maßnahme?" -#: compensation/forms/forms.py:156 +#: compensation/forms/mixins.py:100 #: compensation/templates/compensation/report/compensation/report.html:44 msgid "Is coherence keeping" msgstr "Ist Kohärenzsicherungsmaßnahme" -#: compensation/forms/forms.py:157 +#: compensation/forms/mixins.py:101 msgid "" "Optionally: Whether this compensation is a coherence keeping compensation?" msgstr "Optional: Handelt es sich um eine Kohärenzsicherungsmaßnahme?" -#: compensation/forms/forms.py:169 +#: compensation/forms/mixins.py:113 #: compensation/templates/compensation/detail/compensation/view.html:44 #: compensation/templates/compensation/detail/eco_account/view.html:75 #: compensation/templates/compensation/report/compensation/report.html:24 @@ -467,173 +542,17 @@ msgstr "Optional: Handelt es sich um eine Kohärenzsicherungsmaßnahme?" msgid "Is PIK" msgstr "Ist PIK Maßnahme" -#: compensation/forms/forms.py:170 +#: compensation/forms/mixins.py:114 msgid "" "Optionally: Whether this compensation is a compensation integrated in " "production?" msgstr "Optional: Handelt es sich um eine produktionsintegrierte Kompensation?" -#: compensation/forms/forms.py:190 -#: compensation/templates/compensation/detail/compensation/view.html:36 -#: compensation/templates/compensation/report/compensation/report.html:16 -msgid "compensates intervention" -msgstr "kompensiert Eingriff" - -#: compensation/forms/forms.py:192 -msgid "Select the intervention for which this compensation compensates" -msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist" - -#: compensation/forms/forms.py:219 compensation/views/compensation.py:111 -msgid "New compensation" -msgstr "Neue Kompensation" - -#: compensation/forms/forms.py:292 -msgid "Edit compensation" -msgstr "Bearbeite Kompensation" - -#: compensation/forms/forms.py:356 compensation/utils/quality.py:95 -msgid "Available Surface" -msgstr "Verfügbare Fläche" - -#: compensation/forms/forms.py:359 -msgid "The amount that can be used for deductions" -msgstr "Die für Abbuchungen zur Verfügung stehende Menge" - -#: compensation/forms/forms.py:368 -#: compensation/templates/compensation/detail/eco_account/view.html:67 -#: compensation/utils/quality.py:83 -msgid "Agreement date" -msgstr "Vereinbarungsdatum" - -#: compensation/forms/forms.py:370 -msgid "When did the parties agree on this?" -msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?" - -#: compensation/forms/forms.py:396 compensation/views/eco_account.py:108 -msgid "New Eco-Account" -msgstr "Neues Ökokonto" - -#: compensation/forms/forms.py:405 -msgid "Eco-Account XY; Location ABC" -msgstr "Ökokonto XY; Flur ABC" - -#: compensation/forms/forms.py:467 -msgid "Edit Eco-Account" -msgstr "Ökokonto bearbeiten" - -#: compensation/forms/modalForms.py:38 -msgid "in Euro" -msgstr "in Euro" - -#: compensation/forms/modalForms.py:47 -#: intervention/templates/intervention/detail/includes/payments.html:31 -msgid "Due on" -msgstr "Fällig am" - -#: compensation/forms/modalForms.py:50 -msgid "Due on which date" -msgstr "Zahlung wird an diesem Datum erwartet" - -#: compensation/forms/modalForms.py:65 compensation/forms/modalForms.py:363 -#: intervention/forms/modalForms.py:177 konova/forms.py:479 -msgid "Additional comment, maximum {} letters" -msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" - -#: compensation/forms/modalForms.py:78 -msgid "Add a payment for intervention '{}'" -msgstr "Neue Ersatzzahlung zu Eingriff '{}' hinzufügen" - -#: compensation/forms/modalForms.py:98 -msgid "If there is no date you can enter, please explain why." -msgstr "Falls Sie kein Datum angeben können, erklären Sie bitte weshalb." - -#: compensation/forms/modalForms.py:117 -#: intervention/templates/intervention/detail/includes/payments.html:59 -msgid "Edit payment" -msgstr "Zahlung bearbeiten" - -#: compensation/forms/modalForms.py:161 -msgid "Biotope Type" -msgstr "Biotoptyp" - -#: compensation/forms/modalForms.py:164 -msgid "Select the biotope type" -msgstr "Biotoptyp wählen" - -#: compensation/forms/modalForms.py:168 compensation/forms/modalForms.py:180 -msgid "Biotope additional type" -msgstr "Zusatzbezeichnung" - -#: compensation/forms/modalForms.py:171 -msgid "Select an additional biotope type" -msgstr "Zusatzbezeichnung wählen" - -#: compensation/forms/modalForms.py:190 intervention/forms/modalForms.py:366 -msgid "in m²" -msgstr "" - -#: compensation/forms/modalForms.py:201 -msgid "New state" -msgstr "Neuer Zustand" - -#: compensation/forms/modalForms.py:202 -msgid "Insert data for the new state" -msgstr "Geben Sie die Daten des neuen Zustandes ein" - -#: compensation/forms/modalForms.py:219 konova/forms.py:233 -msgid "Object removed" -msgstr "Objekt entfernt" - -#: compensation/forms/modalForms.py:274 -#: compensation/templates/compensation/detail/compensation/includes/states-after.html:62 -#: compensation/templates/compensation/detail/compensation/includes/states-before.html:62 -#: compensation/templates/compensation/detail/eco_account/includes/states-after.html:62 -#: compensation/templates/compensation/detail/eco_account/includes/states-before.html:62 -#: ema/templates/ema/detail/includes/states-after.html:60 -#: ema/templates/ema/detail/includes/states-before.html:60 -msgid "Edit state" -msgstr "Zustand bearbeiten" - -#: compensation/forms/modalForms.py:333 -msgid "Deadline Type" -msgstr "Fristart" - -#: compensation/forms/modalForms.py:336 -msgid "Select the deadline type" -msgstr "Fristart wählen" - -#: compensation/forms/modalForms.py:345 -#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:36 -#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:36 -#: ema/templates/ema/detail/includes/deadlines.html:36 -#: intervention/forms/modalForms.py:149 konova/forms.py:697 -msgid "Date" -msgstr "Datum" - -#: compensation/forms/modalForms.py:348 -msgid "Select date" -msgstr "Datum wählen" - -#: compensation/forms/modalForms.py:375 -msgid "New deadline" -msgstr "Neue Frist" - -#: compensation/forms/modalForms.py:376 -msgid "Insert data for the new deadline" -msgstr "Geben Sie die Daten der neuen Frist ein" - -#: compensation/forms/modalForms.py:389 -#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:64 -#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:62 -#: ema/templates/ema/detail/includes/deadlines.html:62 -msgid "Edit deadline" -msgstr "Frist/Termin bearbeiten" - -#: compensation/forms/modalForms.py:417 +#: compensation/forms/modals/compensation_action.py:29 msgid "Action Type" msgstr "Maßnahmentyp" -#: compensation/forms/modalForms.py:420 +#: compensation/forms/modals/compensation_action.py:32 msgid "" "An action can consist of multiple different action types. All the selected " "action types are expected to be performed according to the amount and unit " @@ -643,41 +562,154 @@ msgstr "" "hier gewählten Einträge sollen mit der weiter unten angegebenen Einheit und " "Menge umgesetzt werden. " -#: compensation/forms/modalForms.py:425 compensation/forms/modalForms.py:437 +#: compensation/forms/modals/compensation_action.py:37 +#: compensation/forms/modals/compensation_action.py:49 msgid "Action Type detail" msgstr "Zusatzmerkmal" -#: compensation/forms/modalForms.py:428 +#: compensation/forms/modals/compensation_action.py:40 msgid "Select the action type detail" msgstr "Zusatzmerkmal wählen" -#: compensation/forms/modalForms.py:442 +#: compensation/forms/modals/compensation_action.py:54 msgid "Unit" msgstr "Einheit" -#: compensation/forms/modalForms.py:445 +#: compensation/forms/modals/compensation_action.py:57 msgid "Select the unit" msgstr "Einheit wählen" -#: compensation/forms/modalForms.py:457 +#: compensation/forms/modals/compensation_action.py:69 msgid "Insert the amount" msgstr "Menge eingeben" -#: compensation/forms/modalForms.py:482 +#: compensation/forms/modals/compensation_action.py:94 msgid "New action" msgstr "Neue Maßnahme" -#: compensation/forms/modalForms.py:483 +#: compensation/forms/modals/compensation_action.py:95 msgid "Insert data for the new action" msgstr "Geben Sie die Daten der neuen Maßnahme ein" -#: compensation/forms/modalForms.py:507 +#: compensation/forms/modals/compensation_action.py:119 #: compensation/templates/compensation/detail/compensation/includes/actions.html:68 #: compensation/templates/compensation/detail/eco_account/includes/actions.html:67 #: ema/templates/ema/detail/includes/actions.html:65 msgid "Edit action" msgstr "Maßnahme bearbeiten" +#: compensation/forms/modals/deadline.py:21 +msgid "Deadline Type" +msgstr "Fristart" + +#: compensation/forms/modals/deadline.py:24 +msgid "Select the deadline type" +msgstr "Fristart wählen" + +#: compensation/forms/modals/deadline.py:33 +#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:36 +#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:36 +#: ema/templates/ema/detail/includes/deadlines.html:36 +#: intervention/forms/modals/revocation.py:19 +#: konova/forms/modals/resubmission_form.py:22 +msgid "Date" +msgstr "Datum" + +#: compensation/forms/modals/deadline.py:36 +msgid "Select date" +msgstr "Datum wählen" + +#: compensation/forms/modals/deadline.py:51 +#: compensation/forms/modals/payment.py:51 +#: intervention/forms/modals/revocation.py:47 +#: konova/forms/modals/document_form.py:61 +msgid "Additional comment, maximum {} letters" +msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" + +#: compensation/forms/modals/deadline.py:63 +msgid "New deadline" +msgstr "Neue Frist" + +#: compensation/forms/modals/deadline.py:64 +msgid "Insert data for the new deadline" +msgstr "Geben Sie die Daten der neuen Frist ein" + +#: compensation/forms/modals/deadline.py:77 +#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:64 +#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:62 +#: ema/templates/ema/detail/includes/deadlines.html:62 +msgid "Edit deadline" +msgstr "Frist/Termin bearbeiten" + +#: compensation/forms/modals/payment.py:24 +msgid "in Euro" +msgstr "in Euro" + +#: compensation/forms/modals/payment.py:33 +#: intervention/templates/intervention/detail/includes/payments.html:31 +msgid "Due on" +msgstr "Fällig am" + +#: compensation/forms/modals/payment.py:36 +msgid "Due on which date" +msgstr "Zahlung wird an diesem Datum erwartet" + +#: compensation/forms/modals/payment.py:64 +msgid "Add a payment for intervention '{}'" +msgstr "Neue Ersatzzahlung zu Eingriff '{}' hinzufügen" + +#: compensation/forms/modals/payment.py:84 +msgid "If there is no date you can enter, please explain why." +msgstr "Falls Sie kein Datum angeben können, erklären Sie bitte weshalb." + +#: compensation/forms/modals/payment.py:103 +#: intervention/templates/intervention/detail/includes/payments.html:59 +msgid "Edit payment" +msgstr "Zahlung bearbeiten" + +#: compensation/forms/modals/state.py:32 +msgid "Biotope Type" +msgstr "Biotoptyp" + +#: compensation/forms/modals/state.py:35 +msgid "Select the biotope type" +msgstr "Biotoptyp wählen" + +#: compensation/forms/modals/state.py:39 compensation/forms/modals/state.py:51 +msgid "Biotope additional type" +msgstr "Zusatzbezeichnung" + +#: compensation/forms/modals/state.py:42 +msgid "Select an additional biotope type" +msgstr "Zusatzbezeichnung wählen" + +#: compensation/forms/modals/state.py:61 +#: intervention/forms/modals/deduction.py:49 +msgid "in m²" +msgstr "" + +#: compensation/forms/modals/state.py:72 +msgid "New state" +msgstr "Neuer Zustand" + +#: compensation/forms/modals/state.py:73 +msgid "Insert data for the new state" +msgstr "Geben Sie die Daten des neuen Zustandes ein" + +#: compensation/forms/modals/state.py:90 konova/forms/modals/base_form.py:32 +msgid "Object removed" +msgstr "Objekt entfernt" + +#: compensation/forms/modals/state.py:145 +#: compensation/templates/compensation/detail/compensation/includes/states-after.html:62 +#: compensation/templates/compensation/detail/compensation/includes/states-before.html:62 +#: compensation/templates/compensation/detail/eco_account/includes/states-after.html:62 +#: compensation/templates/compensation/detail/eco_account/includes/states-before.html:62 +#: ema/templates/ema/detail/includes/states-after.html:60 +#: ema/templates/ema/detail/includes/states-before.html:60 +msgid "Edit state" +msgstr "Zustand bearbeiten" + #: compensation/models/action.py:20 msgid "cm" msgstr "" @@ -717,27 +749,28 @@ msgstr "" "Es wurde bereits mehr Fläche abgebucht, als Sie nun als abbuchbar einstellen " "wollen. Kontaktieren Sie die für die Abbuchungen verantwortlichen Nutzer!" -#: compensation/tables.py:33 compensation/tables.py:204 ema/tables.py:39 -#: intervention/tables.py:33 konova/filters/mixins.py:98 +#: compensation/tables/compensation.py:33 compensation/tables/eco_account.py:33 +#: ema/tables.py:39 intervention/tables.py:33 +#: konova/filters/mixins/geo_reference.py:42 msgid "Parcel gmrkng" msgstr "Gemarkung" -#: compensation/tables.py:50 compensation/tables.py:225 ema/tables.py:50 -#: intervention/tables.py:50 +#: compensation/tables/compensation.py:50 compensation/tables/eco_account.py:54 +#: ema/tables.py:50 intervention/tables.py:50 msgid "Editable" msgstr "Freigegeben" -#: compensation/tables.py:56 compensation/tables.py:231 ema/tables.py:56 -#: intervention/tables.py:56 +#: compensation/tables/compensation.py:56 compensation/tables/eco_account.py:60 +#: ema/tables.py:56 intervention/tables.py:56 msgid "Last edit" msgstr "Zuletzt bearbeitet" -#: compensation/tables.py:87 compensation/tables.py:263 ema/tables.py:89 -#: intervention/tables.py:87 +#: compensation/tables/compensation.py:87 compensation/tables/eco_account.py:92 +#: ema/tables.py:89 intervention/tables.py:87 msgid "Open {}" msgstr "Öffne {}" -#: compensation/tables.py:163 +#: compensation/tables/compensation.py:163 #: compensation/templates/compensation/detail/compensation/view.html:96 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:58 #: compensation/templates/compensation/detail/eco_account/view.html:48 @@ -747,32 +780,35 @@ msgstr "Öffne {}" msgid "Not recorded yet" msgstr "Noch nicht verzeichnet" -#: compensation/tables.py:166 compensation/tables.py:321 ema/tables.py:133 +#: compensation/tables/compensation.py:166 +#: compensation/tables/eco_account.py:150 ema/tables.py:133 #: intervention/tables.py:164 msgid "Recorded on {} by {}" msgstr "Am {} von {} verzeichnet worden" -#: compensation/tables.py:186 compensation/tables.py:343 ema/tables.py:154 +#: compensation/tables/compensation.py:186 +#: compensation/tables/eco_account.py:172 ema/tables.py:154 #: intervention/tables.py:185 msgid "Full access granted" msgstr "Für Sie freigegeben - Datensatz kann bearbeitet werden" -#: compensation/tables.py:186 compensation/tables.py:343 ema/tables.py:154 +#: compensation/tables/compensation.py:186 +#: compensation/tables/eco_account.py:172 ema/tables.py:154 #: intervention/tables.py:185 msgid "Access not granted" msgstr "Nicht freigegeben - Datensatz nur lesbar" -#: compensation/tables.py:209 +#: compensation/tables/eco_account.py:38 #: compensation/templates/compensation/detail/eco_account/view.html:36 #: konova/templates/konova/widgets/progressbar.html:3 msgid "Available" msgstr "Verfügbar" -#: compensation/tables.py:240 +#: compensation/tables/eco_account.py:69 msgid "Eco Accounts" msgstr "Ökokonten" -#: compensation/tables.py:318 +#: compensation/tables/eco_account.py:147 msgid "Not recorded yet. Can not be used for deductions, yet." msgstr "" "Noch nicht verzeichnet. Kann noch nicht für Abbuchungen genutzt werden." @@ -858,7 +894,8 @@ msgstr "Öffentlicher Bericht" #: compensation/templates/compensation/detail/eco_account/includes/controls.html:15 #: ema/templates/ema/detail/includes/controls.html:15 #: intervention/templates/intervention/detail/includes/controls.html:15 -#: konova/forms.py:724 templates/email/resubmission/resubmission.html:4 +#: konova/forms/modals/resubmission_form.py:49 +#: templates/email/resubmission/resubmission.html:4 msgid "Resubmission" msgstr "Wiedervorlage" @@ -919,7 +956,7 @@ msgstr "Dokumente" #: compensation/templates/compensation/detail/eco_account/includes/documents.html:14 #: ema/templates/ema/detail/includes/documents.html:14 #: intervention/templates/intervention/detail/includes/documents.html:14 -#: konova/forms.py:495 +#: konova/forms/modals/document_form.py:77 msgid "Add new document" msgstr "Neues Dokument hinzufügen" @@ -927,7 +964,7 @@ msgstr "Neues Dokument hinzufügen" #: compensation/templates/compensation/detail/eco_account/includes/documents.html:31 #: ema/templates/ema/detail/includes/documents.html:31 #: intervention/templates/intervention/detail/includes/documents.html:31 -#: konova/forms.py:452 +#: konova/forms/modals/document_form.py:34 msgid "Created on" msgstr "Erstellt" @@ -935,7 +972,7 @@ msgstr "Erstellt" #: compensation/templates/compensation/detail/eco_account/includes/documents.html:61 #: ema/templates/ema/detail/includes/documents.html:61 #: intervention/templates/intervention/detail/includes/documents.html:65 -#: konova/forms.py:557 +#: konova/forms/modals/document_form.py:139 msgid "Edit document" msgstr "Dokument bearbeiten" @@ -1107,7 +1144,7 @@ msgstr "weitere Nutzer" #: compensation/templates/compensation/detail/eco_account/includes/controls.html:18 #: ema/templates/ema/detail/includes/controls.html:18 -#: intervention/forms/modalForms.py:71 +#: intervention/forms/modals/share.py:63 #: intervention/templates/intervention/detail/includes/controls.html:18 msgid "Share" msgstr "Freigabe" @@ -1149,7 +1186,7 @@ msgid "Recorded on" msgstr "Verzeichnet am" #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:65 -#: intervention/forms/modalForms.py:490 +#: intervention/forms/modals/deduction.py:173 #: intervention/templates/intervention/detail/includes/deductions.html:60 msgid "Edit Deduction" msgstr "Abbuchung bearbeiten" @@ -1227,85 +1264,48 @@ msgstr "" msgid "Responsible data" msgstr "Daten zu den verantwortlichen Stellen" -#: compensation/views/compensation.py:54 +#: compensation/views/compensation/compensation.py:56 msgid "Compensations - Overview" msgstr "Kompensationen - Übersicht" -#: compensation/views/compensation.py:173 konova/utils/message_templates.py:36 +#: compensation/views/compensation/compensation.py:175 +#: konova/utils/message_templates.py:37 msgid "Compensation {} edited" msgstr "Kompensation {} bearbeitet" -#: compensation/views/compensation.py:183 compensation/views/eco_account.py:173 -#: ema/views.py:241 intervention/views.py:338 +#: compensation/views/compensation/compensation.py:185 +#: compensation/views/eco_account/eco_account.py:159 ema/views/ema.py:211 +#: intervention/views/intervention.py:225 msgid "Edit {}" msgstr "Bearbeite {}" -#: compensation/views/compensation.py:270 compensation/views/eco_account.py:360 -#: ema/views.py:195 intervention/views.py:565 -msgid "Log" -msgstr "Log" - -#: compensation/views/compensation.py:614 compensation/views/eco_account.py:728 -#: ema/views.py:559 intervention/views.py:711 +#: compensation/views/compensation/report.py:34 +#: compensation/views/eco_account/report.py:34 ema/views/report.py:34 +#: intervention/views/report.py:33 msgid "Report {}" msgstr "Bericht {}" -#: compensation/views/compensation.py:680 compensation/views/eco_account.py:862 -#: ema/views.py:734 intervention/views.py:496 -msgid "Resubmission set" -msgstr "Wiedervorlage gesetzt" - -#: compensation/views/eco_account.py:65 +#: compensation/views/eco_account/eco_account.py:51 msgid "Eco-account - Overview" msgstr "Ökokonten - Übersicht" -#: compensation/views/eco_account.py:98 +#: compensation/views/eco_account/eco_account.py:84 msgid "Eco-Account {} added" msgstr "Ökokonto {} hinzugefügt" -#: compensation/views/eco_account.py:163 +#: compensation/views/eco_account/eco_account.py:149 msgid "Eco-Account {} edited" msgstr "Ökokonto {} bearbeitet" -#: compensation/views/eco_account.py:277 +#: compensation/views/eco_account/eco_account.py:263 msgid "Eco-account removed" msgstr "Ökokonto entfernt" -#: compensation/views/eco_account.py:381 ema/views.py:283 -#: intervention/views.py:664 -msgid "{} unrecorded" -msgstr "{} entzeichnet" - -#: compensation/views/eco_account.py:381 ema/views.py:283 -#: intervention/views.py:664 -msgid "{} recorded" -msgstr "{} verzeichnet" - -#: compensation/views/eco_account.py:805 ema/views.py:629 -#: intervention/views.py:439 -msgid "{} has already been shared with you" -msgstr "{} wurde bereits für Sie freigegeben" - -#: compensation/views/eco_account.py:810 ema/views.py:634 -#: intervention/views.py:444 -msgid "{} has been shared with you" -msgstr "{} ist nun für Sie freigegeben" - -#: compensation/views/eco_account.py:817 ema/views.py:641 -#: intervention/views.py:451 -msgid "Share link invalid" -msgstr "Freigabelink ungültig" - -#: compensation/views/eco_account.py:840 ema/views.py:664 -#: intervention/views.py:474 -msgid "Share settings updated" -msgstr "Freigabe Einstellungen aktualisiert" - -#: ema/forms.py:43 ema/views.py:98 +#: ema/forms.py:42 ema/views/ema.py:94 msgid "New EMA" msgstr "Neue EMA hinzufügen" -#: ema/forms.py:106 +#: ema/forms.py:105 msgid "Edit EMA" msgstr "Bearbeite EMA" @@ -1329,161 +1329,102 @@ msgstr "" msgid "Payment funded compensation" msgstr "Ersatzzahlungsmaßnahme" -#: ema/views.py:55 +#: ema/views/ema.py:51 msgid "EMAs - Overview" msgstr "EMAs - Übersicht" -#: ema/views.py:88 +#: ema/views/ema.py:84 msgid "EMA {} added" msgstr "EMA {} hinzugefügt" -#: ema/views.py:231 +#: ema/views/ema.py:201 msgid "EMA {} edited" msgstr "EMA {} bearbeitet" -#: ema/views.py:264 +#: ema/views/ema.py:234 msgid "EMA removed" msgstr "EMA entfernt" -#: intervention/forms/forms.py:46 +#: intervention/forms/intervention.py:47 msgid "Construction XY; Location ABC" msgstr "Bauvorhaben XY; Flur ABC" -#: intervention/forms/forms.py:52 +#: intervention/forms/intervention.py:53 #: intervention/templates/intervention/detail/view.html:35 #: intervention/templates/intervention/report/report.html:16 #: intervention/utils/quality.py:82 msgid "Process type" msgstr "Verfahrenstyp" -#: intervention/forms/forms.py:71 +#: intervention/forms/intervention.py:72 msgid "Multiple selection possible" msgstr "Mehrfachauswahl möglich" -#: intervention/forms/forms.py:86 +#: intervention/forms/intervention.py:87 #: intervention/templates/intervention/detail/view.html:48 #: intervention/templates/intervention/report/report.html:29 -#: intervention/utils/quality.py:46 konova/filters/mixins.py:371 +#: intervention/utils/quality.py:46 konova/filters/mixins/office.py:66 msgid "Registration office" msgstr "Zulassungsbehörde" -#: intervention/forms/forms.py:118 +#: intervention/forms/intervention.py:119 #: intervention/templates/intervention/detail/view.html:52 #: intervention/templates/intervention/report/report.html:33 #: intervention/utils/quality.py:39 msgid "Registration office file number" msgstr "Aktenzeichen Zulassungsbehörde" -#: intervention/forms/forms.py:124 +#: intervention/forms/intervention.py:125 msgid "ZB-123/ABC.456" msgstr "" -#: intervention/forms/forms.py:142 +#: intervention/forms/intervention.py:143 msgid "Intervention handler type" msgstr "Art des Eingriffsverursachers" -#: intervention/forms/forms.py:144 +#: intervention/forms/intervention.py:145 msgid "What type of handler is responsible for the intervention?" msgstr "Zu welcher Kategorie dieser Eingriffsverursacher gehört" -#: intervention/forms/forms.py:159 +#: intervention/forms/intervention.py:160 msgid "Intervention handler detail" msgstr "Detailangabe zum Eingriffsverursacher" -#: intervention/forms/forms.py:173 +#: intervention/forms/intervention.py:174 #: intervention/templates/intervention/detail/view.html:101 #: intervention/templates/intervention/report/report.html:79 #: intervention/utils/quality.py:73 msgid "Registration date" msgstr "Datum Zulassung bzw. Satzungsbeschluss" -#: intervention/forms/forms.py:185 +#: intervention/forms/intervention.py:186 #: intervention/templates/intervention/detail/view.html:105 #: intervention/templates/intervention/report/report.html:83 msgid "Binding on" msgstr "Datum Bestandskraft bzw. Rechtskraft" -#: intervention/forms/forms.py:211 intervention/views.py:95 +#: intervention/forms/intervention.py:212 intervention/views/intervention.py:98 msgid "New intervention" msgstr "Neuer Eingriff" -#: intervention/forms/forms.py:298 +#: intervention/forms/intervention.py:299 msgid "Edit intervention" msgstr "Eingriff bearbeiten" -#: intervention/forms/modalForms.py:29 -msgid "Share link" -msgstr "Freigabelink" - -#: intervention/forms/modalForms.py:31 -msgid "Send this link to users who you want to have writing access on the data" -msgstr "Einzelne Nutzer erhalten über diesen Link Zugriff auf die Daten" - -#: intervention/forms/modalForms.py:41 -msgid "Add team to share with" -msgstr "Team hinzufügen" - -#: intervention/forms/modalForms.py:43 -msgid "" -"Multiple selection possible - You can only select teams which do not already " -"have access." -msgstr "" -"Mehrfachauswahl möglich - Sie können nur Teams wählen, für die der Eintrag " -"noch nicht freigegeben wurde." - -#: intervention/forms/modalForms.py:55 -msgid "Add user to share with" -msgstr "Nutzer einzeln hinzufügen" - -#: intervention/forms/modalForms.py:57 -msgid "" -"Multiple selection possible - You can only select users which do not already " -"have access. Enter the full username." -msgstr "" -"Mehrfachauswahl möglich - Sie können nur Nutzer wählen, für die der Eintrag " -"noch nicht freigegeben wurde. Geben Sie den ganzen Nutzernamen an." - -#: intervention/forms/modalForms.py:72 -msgid "Share settings for {}" -msgstr "Freigabe Einstellungen für {}" - -#: intervention/forms/modalForms.py:151 -msgid "Date of revocation" -msgstr "Datum des Widerspruchs" - -#: intervention/forms/modalForms.py:162 -#: intervention/templates/intervention/detail/includes/revocation.html:35 -msgid "Document" -msgstr "Dokument" - -#: intervention/forms/modalForms.py:165 -msgid "Must be smaller than 15 Mb" -msgstr "Muss kleiner als 15 Mb sein" - -#: intervention/forms/modalForms.py:190 -#: intervention/templates/intervention/detail/includes/revocation.html:18 -msgid "Add revocation" -msgstr "Widerspruch hinzufügen" - -#: intervention/forms/modalForms.py:208 -#: intervention/templates/intervention/detail/includes/revocation.html:69 -msgid "Edit revocation" -msgstr "Widerspruch bearbeiten" - -#: intervention/forms/modalForms.py:248 +#: intervention/forms/modals/check.py:20 msgid "Checked intervention data" msgstr "Eingriffsdaten geprüft" -#: intervention/forms/modalForms.py:254 +#: intervention/forms/modals/check.py:26 msgid "Checked compensations data and payments" msgstr "Kompensationen und Zahlungen geprüft" -#: intervention/forms/modalForms.py:263 +#: intervention/forms/modals/check.py:35 #: intervention/templates/intervention/detail/includes/controls.html:22 msgid "Run check" msgstr "Prüfung vornehmen" -#: intervention/forms/modalForms.py:264 konova/forms.py:598 +#: intervention/forms/modals/check.py:36 konova/forms/modals/record_form.py:30 msgid "" "I, {} {}, confirm that all necessary control steps have been performed by " "myself." @@ -1491,23 +1432,23 @@ msgstr "" "Ich, {} {}, bestätige, dass die notwendigen Kontrollschritte durchgeführt " "wurden:" -#: intervention/forms/modalForms.py:350 +#: intervention/forms/modals/deduction.py:33 msgid "Only recorded accounts can be selected for deductions" msgstr "Nur verzeichnete Ökokonten können für Abbuchungen verwendet werden." -#: intervention/forms/modalForms.py:377 +#: intervention/forms/modals/deduction.py:60 msgid "Only shared interventions can be selected" msgstr "Nur freigegebene Eingriffe können gewählt werden" -#: intervention/forms/modalForms.py:390 +#: intervention/forms/modals/deduction.py:73 msgid "New Deduction" msgstr "Neue Abbuchung" -#: intervention/forms/modalForms.py:391 +#: intervention/forms/modals/deduction.py:74 msgid "Enter the information for a new deduction from a chosen eco-account" msgstr "Geben Sie die Informationen für eine neue Abbuchung ein." -#: intervention/forms/modalForms.py:436 +#: intervention/forms/modals/deduction.py:119 msgid "" "Eco-account {} is not recorded yet. You can only deduct from recorded " "accounts." @@ -1515,7 +1456,7 @@ msgstr "" "Ökokonto {} ist noch nicht verzeichnet. Abbuchungen können nur von " "verzeichneten Ökokonten erfolgen." -#: intervention/forms/modalForms.py:443 +#: intervention/forms/modals/deduction.py:126 msgid "" "Intervention {} is currently recorded. To change any data on it, the entry " "must be unrecorded." @@ -1523,7 +1464,7 @@ msgstr "" "Eingriff {} ist verzeichnet. Der Eintrag muss erst entzeichnet werden um " "fortfahren zu können." -#: intervention/forms/modalForms.py:453 +#: intervention/forms/modals/deduction.py:136 msgid "" "The account {} has not enough surface for a deduction of {} m². There are " "only {} m² left" @@ -1531,6 +1472,65 @@ msgstr "" "Das Ökokonto {} hat für eine Abbuchung von {} m² nicht ausreichend " "Restfläche. Es stehen noch {} m² zur Verfügung." +#: intervention/forms/modals/revocation.py:21 +msgid "Date of revocation" +msgstr "Datum des Widerspruchs" + +#: intervention/forms/modals/revocation.py:32 +#: intervention/templates/intervention/detail/includes/revocation.html:35 +msgid "Document" +msgstr "Dokument" + +#: intervention/forms/modals/revocation.py:35 +msgid "Must be smaller than 15 Mb" +msgstr "Muss kleiner als 15 Mb sein" + +#: intervention/forms/modals/revocation.py:60 +#: intervention/templates/intervention/detail/includes/revocation.html:18 +msgid "Add revocation" +msgstr "Widerspruch hinzufügen" + +#: intervention/forms/modals/revocation.py:78 +#: intervention/templates/intervention/detail/includes/revocation.html:69 +msgid "Edit revocation" +msgstr "Widerspruch bearbeiten" + +#: intervention/forms/modals/share.py:21 +msgid "Share link" +msgstr "Freigabelink" + +#: intervention/forms/modals/share.py:23 +msgid "Send this link to users who you want to have writing access on the data" +msgstr "Einzelne Nutzer erhalten über diesen Link Zugriff auf die Daten" + +#: intervention/forms/modals/share.py:33 +msgid "Add team to share with" +msgstr "Team hinzufügen" + +#: intervention/forms/modals/share.py:35 +msgid "" +"Multiple selection possible - You can only select teams which do not already " +"have access." +msgstr "" +"Mehrfachauswahl möglich - Sie können nur Teams wählen, für die der Eintrag " +"noch nicht freigegeben wurde." + +#: intervention/forms/modals/share.py:47 +msgid "Add user to share with" +msgstr "Nutzer einzeln hinzufügen" + +#: intervention/forms/modals/share.py:49 +msgid "" +"Multiple selection possible - You can only select users which do not already " +"have access. Enter the full username." +msgstr "" +"Mehrfachauswahl möglich - Sie können nur Nutzer wählen, für die der Eintrag " +"noch nicht freigegeben wurde. Geben Sie den ganzen Nutzernamen an." + +#: intervention/forms/modals/share.py:64 +msgid "Share settings for {}" +msgstr "Freigabe Einstellungen für {}" + #: intervention/templates/intervention/detail/includes/compensations.html:14 msgid "Add new compensation" msgstr "Neue Kompensation hinzufügen" @@ -1623,39 +1623,35 @@ msgstr "" "Kein Ausgleich jeglicher Art gefunden (Kompensation, Ersatzzahlung, " "Abbuchung)" -#: intervention/views.py:52 -msgid "Interventions - Overview" -msgstr "Eingriffe - Übersicht" - -#: intervention/views.py:85 -msgid "Intervention {} added" -msgstr "Eingriff {} hinzugefügt" - -#: intervention/views.py:326 -msgid "Intervention {} edited" -msgstr "Eingriff {} bearbeitet" - -#: intervention/views.py:362 -msgid "{} removed" -msgstr "{} entfernt" - -#: intervention/views.py:518 +#: intervention/views/check.py:36 msgid "Check performed" msgstr "Prüfung durchgeführt" -#: intervention/views.py:669 -msgid "There are errors on this intervention:" -msgstr "Es liegen Fehler in diesem Eingriff vor:" +#: intervention/views/intervention.py:55 +msgid "Interventions - Overview" +msgstr "Eingriffe - Übersicht" -#: konova/decorators.py:30 +#: intervention/views/intervention.py:88 +msgid "Intervention {} added" +msgstr "Eingriff {} hinzugefügt" + +#: intervention/views/intervention.py:213 +msgid "Intervention {} edited" +msgstr "Eingriff {} bearbeitet" + +#: intervention/views/intervention.py:249 +msgid "{} removed" +msgstr "{} entfernt" + +#: konova/decorators.py:33 msgid "You need to be staff to perform this action!" msgstr "Hierfür müssen Sie Mitarbeiter sein!" -#: konova/decorators.py:45 +#: konova/decorators.py:48 msgid "You need to be administrator to perform this action!" msgstr "Hierfür müssen Sie Administrator sein!" -#: konova/decorators.py:63 +#: konova/decorators.py:66 msgid "" "+++ Attention: You are not part of any group. You won't be able to create, " "edit or do anything. Please contact an administrator. +++" @@ -1664,154 +1660,158 @@ msgstr "" "somit nichts eingeben, bearbeiten oder sonstige Aktionen ausführen. " "Kontaktieren Sie bitte einen Administrator. +++" -#: konova/filters/mixins.py:57 +#: konova/decorators.py:169 +msgid "Session timed out" +msgstr "Sitzung abgelaufen" + +#: konova/filters/mixins/file_number.py:21 msgid "File number" msgstr "Aktenzeichen" -#: konova/filters/mixins.py:58 +#: konova/filters/mixins/file_number.py:22 msgid "Search for file number" msgstr "Nach Aktenzeichen suchen" -#: konova/filters/mixins.py:85 +#: konova/filters/mixins/geo_reference.py:29 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:18 msgid "District" msgstr "Kreis" -#: konova/filters/mixins.py:86 +#: konova/filters/mixins/geo_reference.py:30 msgid "Search for district" msgstr "Nach Kreis suchen" -#: konova/filters/mixins.py:99 +#: konova/filters/mixins/geo_reference.py:43 msgid "Search for parcel gmrkng" msgstr "Nach Gemarkung suchen" -#: konova/filters/mixins.py:111 +#: konova/filters/mixins/geo_reference.py:55 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:39 msgid "Parcel" msgstr "Flur" -#: konova/filters/mixins.py:112 +#: konova/filters/mixins/geo_reference.py:56 msgid "Search for parcel" msgstr "Nach Flur suchen" -#: konova/filters/mixins.py:124 +#: konova/filters/mixins/geo_reference.py:68 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:40 msgid "Parcel counter" msgstr "Flurstückzähler" -#: konova/filters/mixins.py:125 +#: konova/filters/mixins/geo_reference.py:69 msgid "Search for parcel counter" msgstr "Nach Flurstückzähler suchen" -#: konova/filters/mixins.py:138 +#: konova/filters/mixins/geo_reference.py:82 #: konova/templates/konova/includes/parcels/parcel_table_frame.html:41 msgid "Parcel number" msgstr "Flurstücknenner" -#: konova/filters/mixins.py:139 +#: konova/filters/mixins/geo_reference.py:83 msgid "Search for parcel number" msgstr "Nach Flurstücknenner suchen" -#: konova/filters/mixins.py:276 -msgid "Show unshared" -msgstr "Nicht freigegebene anzeigen" - -#: konova/filters/mixins.py:322 -msgid "Show recorded" -msgstr "Verzeichnete anzeigen" - -#: konova/filters/mixins.py:372 -msgid "Search for registration office" -msgstr "Nach Zulassungsbehörde suchen" - -#: konova/filters/mixins.py:404 +#: konova/filters/mixins/office.py:35 msgid "Search for conservation office" msgstr "Nch Eintragungsstelle suchen" -#: konova/forms.py:44 templates/form/collapsable/form.html:62 +#: konova/filters/mixins/office.py:67 +msgid "Search for registration office" +msgstr "Nach Zulassungsbehörde suchen" + +#: konova/filters/mixins/record.py:22 +msgid "Show recorded" +msgstr "Verzeichnete anzeigen" + +#: konova/filters/mixins/share.py:22 +msgid "Show unshared" +msgstr "Nicht freigegebene anzeigen" + +#: konova/forms/base_form.py:23 templates/form/collapsable/form.html:62 msgid "Save" msgstr "Speichern" -#: konova/forms.py:78 +#: konova/forms/base_form.py:57 msgid "Not editable" msgstr "Nicht editierbar" -#: konova/forms.py:182 konova/forms.py:398 -msgid "Confirm" -msgstr "Bestätige" - -#: konova/forms.py:194 konova/forms.py:407 -msgid "Remove" -msgstr "Löschen" - -#: konova/forms.py:196 -msgid "You are about to remove {} {}" -msgstr "Sie sind dabei {} {} zu löschen" - -#: konova/forms.py:284 konova/utils/quality.py:44 konova/utils/quality.py:46 -#: templates/form/collapsable/form.html:45 +#: konova/forms/geometry_form.py:29 konova/utils/quality.py:44 +#: konova/utils/quality.py:46 templates/form/collapsable/form.html:45 msgid "Geometry" msgstr "Geometrie" -#: konova/forms.py:335 +#: konova/forms/geometry_form.py:80 msgid "Only surfaces allowed. Points or lines must be buffered." msgstr "" "Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden." -#: konova/forms.py:408 -msgid "Are you sure?" -msgstr "Sind Sie sicher?" - -#: konova/forms.py:454 +#: konova/forms/modals/document_form.py:36 msgid "When has this file been created? Important for photos." msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?" -#: konova/forms.py:465 +#: konova/forms/modals/document_form.py:47 #: venv/lib/python3.7/site-packages/django/db/models/fields/files.py:231 msgid "File" msgstr "Datei" -#: konova/forms.py:467 +#: konova/forms/modals/document_form.py:49 msgid "Allowed formats: pdf, jpg, png. Max size 15 MB." msgstr "Formate: pdf, jpg, png. Maximal 15 MB." -#: konova/forms.py:532 +#: konova/forms/modals/document_form.py:114 msgid "Added document" msgstr "Dokument hinzugefügt" -#: konova/forms.py:589 +#: konova/forms/modals/record_form.py:21 msgid "Confirm record" msgstr "Verzeichnen bestätigen" -#: konova/forms.py:597 +#: konova/forms/modals/record_form.py:29 msgid "Record data" msgstr "Daten verzeichnen" -#: konova/forms.py:604 +#: konova/forms/modals/record_form.py:36 msgid "Confirm unrecord" msgstr "Entzeichnen bestätigen" -#: konova/forms.py:605 +#: konova/forms/modals/record_form.py:37 msgid "Unrecord data" msgstr "Daten entzeichnen" -#: konova/forms.py:606 +#: konova/forms/modals/record_form.py:38 msgid "I, {} {}, confirm that this data must be unrecorded." msgstr "" "Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen." -#: konova/forms.py:698 +#: konova/forms/modals/remove_form.py:22 konova/forms/remove_form.py:18 +msgid "Confirm" +msgstr "Bestätige" + +#: konova/forms/modals/remove_form.py:31 konova/forms/remove_form.py:30 +msgid "Remove" +msgstr "Löschen" + +#: konova/forms/modals/remove_form.py:32 +msgid "Are you sure?" +msgstr "Sind Sie sicher?" + +#: konova/forms/modals/resubmission_form.py:23 msgid "When do you want to be reminded?" msgstr "Wann wollen Sie erinnert werden?" -#: konova/forms.py:725 +#: konova/forms/modals/resubmission_form.py:50 msgid "Set your resubmission for this entry." msgstr "Setzen Sie eine Wiedervorlage für diesen Eintrag." -#: konova/forms.py:746 +#: konova/forms/modals/resubmission_form.py:71 msgid "The date should be in the future" msgstr "Das Datum sollte in der Zukunft liegen" +#: konova/forms/remove_form.py:32 +msgid "You are about to remove {} {}" +msgstr "Sie sind dabei {} {} zu löschen" + #: konova/management/commands/setup_data.py:26 msgid "On shared access gained" msgstr "Wenn mir eine Freigabe zu Daten erteilt wird" @@ -2043,15 +2043,19 @@ msgstr "" "bedeutet, dass Sie nur lesenden Zugriff hierauf haben und weder bearbeiten, " "noch Prüfungen durchführen oder verzeichnen können." -#: konova/utils/message_templates.py:27 +#: konova/utils/message_templates.py:25 +msgid "Share settings updated" +msgstr "Freigabe Einstellungen aktualisiert" + +#: konova/utils/message_templates.py:28 msgid "Unsupported file type" msgstr "Dateiformat nicht unterstützt" -#: konova/utils/message_templates.py:28 +#: konova/utils/message_templates.py:29 msgid "File too large" msgstr "Datei zu groß" -#: konova/utils/message_templates.py:31 +#: konova/utils/message_templates.py:32 msgid "" "Action canceled. Eco account is recorded or deductions exist. Only " "conservation office member can perform this action." @@ -2059,132 +2063,136 @@ msgstr "" "Aktion abgebrochen. Ökokonto ist bereits verzeichnet oder Abbuchungen liegen " "vor. Nur Eintragungsstellennutzer können diese Aktion jetzt durchführen." -#: konova/utils/message_templates.py:34 +#: konova/utils/message_templates.py:35 msgid "Compensation {} added" msgstr "Kompensation {} hinzugefügt" -#: konova/utils/message_templates.py:35 +#: konova/utils/message_templates.py:36 msgid "Compensation {} removed" msgstr "Kompensation {} entfernt" -#: konova/utils/message_templates.py:37 +#: konova/utils/message_templates.py:38 msgid "Added compensation action" msgstr "Maßnahme hinzugefügt" -#: konova/utils/message_templates.py:38 +#: konova/utils/message_templates.py:39 msgid "Added compensation state" msgstr "Zustand hinzugefügt" -#: konova/utils/message_templates.py:41 +#: konova/utils/message_templates.py:42 msgid "State removed" msgstr "Zustand gelöscht" -#: konova/utils/message_templates.py:42 +#: konova/utils/message_templates.py:43 msgid "State edited" msgstr "Zustand bearbeitet" -#: konova/utils/message_templates.py:43 +#: konova/utils/message_templates.py:44 msgid "State added" msgstr "Zustand hinzugefügt" -#: konova/utils/message_templates.py:46 +#: konova/utils/message_templates.py:47 msgid "Action added" msgstr "Maßnahme hinzugefügt" -#: konova/utils/message_templates.py:47 +#: konova/utils/message_templates.py:48 msgid "Action edited" msgstr "Maßnahme bearbeitet" -#: konova/utils/message_templates.py:48 +#: konova/utils/message_templates.py:49 msgid "Action removed" msgstr "Maßnahme entfernt" -#: konova/utils/message_templates.py:51 +#: konova/utils/message_templates.py:52 msgid "Deduction added" msgstr "Abbuchung hinzugefügt" -#: konova/utils/message_templates.py:52 +#: konova/utils/message_templates.py:53 msgid "Deduction edited" msgstr "Abbuchung bearbeitet" -#: konova/utils/message_templates.py:53 +#: konova/utils/message_templates.py:54 msgid "Deduction removed" msgstr "Abbuchung entfernt" -#: konova/utils/message_templates.py:56 +#: konova/utils/message_templates.py:55 +msgid "Unknown deduction" +msgstr "Unbekannte Abbuchung" + +#: konova/utils/message_templates.py:58 msgid "Deadline added" msgstr "Frist/Termin hinzugefügt" -#: konova/utils/message_templates.py:57 +#: konova/utils/message_templates.py:59 msgid "Deadline edited" msgstr "Frist/Termin bearbeitet" -#: konova/utils/message_templates.py:58 +#: konova/utils/message_templates.py:60 msgid "Deadline removed" msgstr "Frist/Termin gelöscht" -#: konova/utils/message_templates.py:61 +#: konova/utils/message_templates.py:63 msgid "Payment added" msgstr "Zahlung hinzugefügt" -#: konova/utils/message_templates.py:62 +#: konova/utils/message_templates.py:64 msgid "Payment edited" msgstr "Zahlung bearbeitet" -#: konova/utils/message_templates.py:63 +#: konova/utils/message_templates.py:65 msgid "Payment removed" msgstr "Zahlung gelöscht" -#: konova/utils/message_templates.py:66 +#: konova/utils/message_templates.py:68 msgid "Revocation added" msgstr "Widerspruch hinzugefügt" -#: konova/utils/message_templates.py:67 +#: konova/utils/message_templates.py:69 msgid "Revocation edited" msgstr "Widerspruch bearbeitet" -#: konova/utils/message_templates.py:68 +#: konova/utils/message_templates.py:70 msgid "Revocation removed" msgstr "Widerspruch entfernt" -#: konova/utils/message_templates.py:71 +#: konova/utils/message_templates.py:73 msgid "Document '{}' deleted" msgstr "Dokument '{}' gelöscht" -#: konova/utils/message_templates.py:72 +#: konova/utils/message_templates.py:74 msgid "Document added" msgstr "Dokument hinzugefügt" -#: konova/utils/message_templates.py:73 +#: konova/utils/message_templates.py:75 msgid "Document edited" msgstr "Dokument bearbeitet" -#: konova/utils/message_templates.py:76 +#: konova/utils/message_templates.py:78 msgid "Edited general data" msgstr "Allgemeine Daten bearbeitet" -#: konova/utils/message_templates.py:77 +#: konova/utils/message_templates.py:79 msgid "Added deadline" msgstr "Frist/Termin hinzugefügt" -#: konova/utils/message_templates.py:80 +#: konova/utils/message_templates.py:82 msgid "Geometry conflict detected with {}" msgstr "Geometriekonflikt mit folgenden Einträgen erkannt: {}" -#: konova/utils/message_templates.py:83 +#: konova/utils/message_templates.py:85 msgid "This intervention has {} revocations" msgstr "Dem Eingriff liegen {} Widersprüche vor" -#: konova/utils/message_templates.py:86 +#: konova/utils/message_templates.py:88 msgid "Checked on {} by {}" msgstr "Am {} von {} geprüft worden" -#: konova/utils/message_templates.py:87 +#: konova/utils/message_templates.py:89 msgid "Data has changed since last check on {} by {}" msgstr "" "Daten wurden nach der letzten Prüfung geändert. Letzte Prüfung am {} durch {}" -#: konova/utils/message_templates.py:88 +#: konova/utils/message_templates.py:90 msgid "Current data not checked yet" msgstr "Momentane Daten noch nicht geprüft" @@ -2204,10 +2212,42 @@ msgstr "{} wurde erfolgreich vom Nutzer {} geprüft! {}" msgid "missing" msgstr "fehlt" -#: konova/views.py:99 templates/navbars/navbar.html:16 +#: konova/views/home.py:78 templates/navbars/navbar.html:16 msgid "Home" msgstr "Home" +#: konova/views/log.py:38 +msgid "Log" +msgstr "Log" + +#: konova/views/record.py:30 +msgid "{} unrecorded" +msgstr "{} entzeichnet" + +#: konova/views/record.py:30 +msgid "{} recorded" +msgstr "{} verzeichnet" + +#: konova/views/record.py:35 +msgid "Errors found:" +msgstr "Fehler gefunden:" + +#: konova/views/resubmission.py:39 +msgid "Resubmission set" +msgstr "Wiedervorlage gesetzt" + +#: konova/views/share.py:46 +msgid "{} has already been shared with you" +msgstr "{} wurde bereits für Sie freigegeben" + +#: konova/views/share.py:51 +msgid "{} has been shared with you" +msgstr "{} ist nun für Sie freigegeben" + +#: konova/views/share.py:58 +msgid "Share link invalid" +msgstr "Freigabelink ungültig" + #: news/models.py:12 msgid "Default" msgstr "Standard" @@ -2621,6 +2661,10 @@ msgstr "Nutzer" msgid "No geometry added, yet." msgstr "Keine Geometrie vorhanden" +#: templates/modal/modal_session_timed_out.html:3 +msgid "Your session has timed out. Please reload the page to login." +msgstr "Ihre Sitzung ist aufgrund von Inaktivität abgelaufen. Laden Sie die Seite erneut, um sich wieder einzuloggen." + #: templates/navbars/navbar.html:4 msgid "Kompensationsverzeichnis Service Portal" msgstr "" @@ -2674,59 +2718,21 @@ msgstr "" "Falls die Geometrie nicht leer ist, werden die Flurstücke aktuell berechnet. " "Bitte laden Sie diese Seite in ein paar Augenblicken erneut..." -#: user/forms.py:27 -msgid "Notifications" -msgstr "Benachrichtigungen" - -#: user/forms.py:29 -msgid "Select the situations when you want to receive a notification" -msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?" - -#: user/forms.py:41 -msgid "Edit notifications" -msgstr "Benachrichtigungen bearbeiten" - -#: user/forms.py:76 user/templates/user/index.html:9 -msgid "Username" -msgstr "Nutzername" - -#: user/forms.py:87 -msgid "Person name" -msgstr "Name" - -#: user/forms.py:98 user/templates/user/index.html:17 -msgid "E-Mail" -msgstr "" - -#: user/forms.py:112 -msgid "User contact data" -msgstr "Kontaktdaten" - -#: user/forms.py:122 -msgid "Token" -msgstr "" - -#: user/forms.py:137 -msgid "Create new token" -msgstr "Neuen Token generieren" - -#: user/forms.py:138 -msgid "A new token needs to be validated by an administrator!" -msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!" - -#: user/forms.py:168 user/forms.py:172 user/forms.py:354 user/forms.py:359 +#: user/forms/modals/team.py:20 user/forms/modals/team.py:24 +#: user/forms/team.py:17 user/forms/team.py:22 msgid "Team name" msgstr "Team Name" -#: user/forms.py:179 user/forms.py:367 user/templates/user/team/index.html:30 +#: user/forms/modals/team.py:31 user/forms/team.py:30 +#: user/templates/user/team/index.html:30 msgid "Description" msgstr "Beschreibung" -#: user/forms.py:188 +#: user/forms/modals/team.py:40 msgid "Manage team members" msgstr "Mitglieder verwalten" -#: user/forms.py:190 +#: user/forms/modals/team.py:42 msgid "" "Multiple selection possible - You can only select users which are not " "already a team member. Enter the full username or e-mail." @@ -2734,11 +2740,11 @@ msgstr "" "Mehrfachauswahl möglich - Sie können nur Nutzer wählen, die noch nicht " "Mitglieder dieses Teams sind. Geben Sie den ganzen Nutzernamen an." -#: user/forms.py:204 +#: user/forms/modals/team.py:56 msgid "Create new team" msgstr "Neues Team anlegen" -#: user/forms.py:205 +#: user/forms/modals/team.py:57 msgid "" "You will become the administrator for this group by default. You do not need " "to add yourself to the list of members." @@ -2746,31 +2752,31 @@ msgstr "" "Sie werden standardmäßig der Administrator dieses Teams. Sie müssen sich " "selbst nicht zur Liste der Mitglieder hinzufügen." -#: user/forms.py:218 user/forms.py:296 +#: user/forms/modals/team.py:70 user/forms/modals/team.py:148 msgid "Name already taken. Try another." msgstr "Name bereits vergeben. Probieren Sie einen anderen." -#: user/forms.py:248 +#: user/forms/modals/team.py:100 msgid "Admins" msgstr "Administratoren" -#: user/forms.py:250 +#: user/forms/modals/team.py:102 msgid "Administrators manage team details and members" msgstr "Administratoren verwalten die Teamdaten und Mitglieder" -#: user/forms.py:273 +#: user/forms/modals/team.py:125 msgid "Selected admins need to be members of this team." msgstr "Gewählte Administratoren müssen Teammitglieder sein." -#: user/forms.py:280 +#: user/forms/modals/team.py:132 msgid "There must be at least one admin on this team." msgstr "Es muss mindestens einen Administrator für das Team geben." -#: user/forms.py:308 user/templates/user/team/index.html:60 +#: user/forms/modals/team.py:160 user/templates/user/team/index.html:60 msgid "Edit team" msgstr "Team bearbeiten" -#: user/forms.py:335 +#: user/forms/modals/team.py:187 msgid "" "ATTENTION!\n" "\n" @@ -2786,14 +2792,54 @@ msgstr "" "\n" "Sind Sie sicher, dass Sie dieses Team löschen möchten?" -#: user/forms.py:345 user/templates/user/team/index.html:56 +#: user/forms/modals/team.py:197 user/templates/user/team/index.html:56 msgid "Leave team" msgstr "Team verlassen" -#: user/forms.py:378 +#: user/forms/modals/user.py:20 user/templates/user/index.html:9 +msgid "Username" +msgstr "Nutzername" + +#: user/forms/modals/user.py:31 +msgid "Person name" +msgstr "Name" + +#: user/forms/modals/user.py:42 user/templates/user/index.html:17 +msgid "E-Mail" +msgstr "" + +#: user/forms/modals/user.py:56 +msgid "User contact data" +msgstr "Kontaktdaten" + +#: user/forms/team.py:41 msgid "Team" msgstr "Team" +#: user/forms/user.py:24 +msgid "Notifications" +msgstr "Benachrichtigungen" + +#: user/forms/user.py:26 +msgid "Select the situations when you want to receive a notification" +msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?" + +#: user/forms/user.py:38 +msgid "Edit notifications" +msgstr "Benachrichtigungen bearbeiten" + +#: user/forms/user.py:73 +msgid "Token" +msgstr "" + +#: user/forms/user.py:88 +msgid "Create new token" +msgstr "Neuen Token generieren" + +#: user/forms/user.py:89 +msgid "A new token needs to be validated by an administrator!" +msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!" + #: user/models/user_action.py:23 msgid "Unrecorded" msgstr "Entzeichnet" @@ -2856,7 +2902,7 @@ msgid "Manage teams" msgstr "" #: user/templates/user/index.html:61 user/templates/user/team/index.html:19 -#: user/views.py:167 +#: user/views.py:169 msgid "Teams" msgstr "" @@ -2908,43 +2954,43 @@ msgstr "Token noch nicht freigeschaltet" msgid "Valid until" msgstr "Läuft ab am" -#: user/views.py:33 +#: user/views.py:35 msgid "User settings" msgstr "Einstellungen" -#: user/views.py:59 +#: user/views.py:61 msgid "Notifications edited" msgstr "Benachrichtigungen bearbeitet" -#: user/views.py:71 +#: user/views.py:73 msgid "User notifications" msgstr "Benachrichtigungen" -#: user/views.py:94 +#: user/views.py:96 msgid "New token generated. Administrators need to validate." msgstr "Neuer Token generiert. Administratoren sind informiert." -#: user/views.py:105 +#: user/views.py:107 msgid "User API token" msgstr "API Nutzer Token" -#: user/views.py:178 +#: user/views.py:180 msgid "New team added" msgstr "Neues Team hinzugefügt" -#: user/views.py:192 +#: user/views.py:194 msgid "Team edited" msgstr "Team bearbeitet" -#: user/views.py:206 +#: user/views.py:208 msgid "Team removed" msgstr "Team gelöscht" -#: user/views.py:220 +#: user/views.py:222 msgid "You are not a member of this team" msgstr "Sie sind kein Mitglied dieses Teams" -#: user/views.py:227 +#: user/views.py:229 msgid "Left Team" msgstr "Team verlassen" @@ -4447,6 +4493,9 @@ msgstr "" msgid "Unable to connect to qpid with SASL mechanism %s" msgstr "" +#~ msgid "There are errors on this intervention:" +#~ msgstr "Es liegen Fehler in diesem Eingriff vor:" + #~ msgid "Before" #~ msgstr "Vor" diff --git a/templates/modal/modal_session_timed_out.html b/templates/modal/modal_session_timed_out.html new file mode 100644 index 00000000..8a2fdad1 --- /dev/null +++ b/templates/modal/modal_session_timed_out.html @@ -0,0 +1,3 @@ +{% load i18n %} + +{% trans 'Your session has timed out. Please reload the page to login.' %} \ No newline at end of file diff --git a/user/views.py b/user/views.py index 7e2d21e6..30ce627e 100644 --- a/user/views.py +++ b/user/views.py @@ -15,7 +15,7 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.utils.translation import gettext_lazy as _ from konova.contexts import BaseContext -from konova.decorators import any_group_check, default_group_required +from konova.decorators import any_group_check, default_group_required, login_required_modal @login_required @@ -110,6 +110,7 @@ def api_token_view(request: HttpRequest): return render(request, template, context) +@login_required_modal @login_required def contact_view(request: HttpRequest, id: str): """ Renders contact modal view of a users contact data @@ -135,6 +136,7 @@ def contact_view(request: HttpRequest, id: str): ) +@login_required_modal @login_required def data_team_view(request: HttpRequest, id: str): """ Renders team data @@ -172,6 +174,7 @@ def index_team_view(request: HttpRequest): return render(request, template, context) +@login_required_modal @login_required def new_team_view(request: HttpRequest): form = NewTeamModalForm(request.POST or None, request=request) @@ -182,6 +185,7 @@ def new_team_view(request: HttpRequest): ) +@login_required_modal @login_required def edit_team_view(request: HttpRequest, id: str): team = get_object_or_404(Team, id=id) @@ -196,6 +200,7 @@ def edit_team_view(request: HttpRequest, id: str): ) +@login_required_modal @login_required def remove_team_view(request: HttpRequest, id: str): team = get_object_or_404(Team, id=id) @@ -210,6 +215,7 @@ def remove_team_view(request: HttpRequest, id: str): ) +@login_required_modal @login_required def leave_team_view(request: HttpRequest, id: str): team = get_object_or_404(Team, id=id) From a8b7cebc098ded675dc0eef3386953829e85a6a0 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 12 Sep 2022 13:12:59 +0200 Subject: [PATCH 33/48] Netgis client update * adds new version to sources --- konova/views/map_proxy.py | 9 +- templates/map/client/config.json | 13 + templates/map/client/index.html | 31 +- .../5.12.0/webfonts/fa-brands-400.eot | Bin 131930 -> 0 bytes .../5.12.0/webfonts/fa-brands-400.ttf | Bin 131624 -> 0 bytes .../5.12.0/webfonts/fa-brands-400.woff | Bin 89100 -> 0 bytes .../5.12.0/webfonts/fa-brands-400.woff2 | Bin 75936 -> 0 bytes .../5.12.0/webfonts/fa-regular-400.eot | Bin 34390 -> 0 bytes .../5.12.0/webfonts/fa-regular-400.ttf | Bin 34092 -> 0 bytes .../5.12.0/webfonts/fa-regular-400.woff | Bin 16800 -> 0 bytes .../5.12.0/webfonts/fa-regular-400.woff2 | Bin 13576 -> 0 bytes .../5.12.0/webfonts/fa-solid-900.eot | Bin 194066 -> 0 bytes .../5.12.0/webfonts/fa-solid-900.ttf | Bin 193780 -> 0 bytes .../5.12.0/webfonts/fa-solid-900.woff | Bin 98996 -> 0 bytes .../5.12.0/webfonts/fa-solid-900.woff2 | Bin 76084 -> 0 bytes .../client/libs/jspdf/1.3.2/jspdf.debug.js | 412 ++-- .../map/client/libs/netgis/Attribution.css | 11 + .../map/client/libs/netgis/Attribution.js | 86 + templates/map/client/libs/netgis/Client.css | 47 + templates/map/client/libs/netgis/Client.js | 302 +++ templates/map/client/libs/netgis/Controls.css | 59 + templates/map/client/libs/netgis/Controls.js | 59 + templates/map/client/libs/netgis/Events.js | 68 + .../map/client/libs/netgis/LayerTree.css | 159 ++ templates/map/client/libs/netgis/LayerTree.js | 394 ++++ .../map/client/libs/netgis/LayerTypes.js | 14 + templates/map/client/libs/netgis/Map.css | 102 + templates/map/client/libs/netgis/Map.js | 20 + .../map/client/libs/netgis/MapOpenLayers.js | 1665 +++++++++++++++++ templates/map/client/libs/netgis/Menu.css | 118 ++ templates/map/client/libs/netgis/Menu.js | 249 +++ templates/map/client/libs/netgis/Modal.css | 160 ++ templates/map/client/libs/netgis/Modal.js | 592 ++++++ templates/map/client/libs/netgis/Modes.js | 27 + templates/map/client/libs/netgis/OWS.js | 237 +++ templates/map/client/libs/netgis/SLD.js | 79 + .../map/client/libs/netgis/SearchParcel.css | 131 ++ .../map/client/libs/netgis/SearchParcel.js | 428 +++++ .../map/client/libs/netgis/SearchPlace.js | 59 + templates/map/client/libs/netgis/Theme.css | 69 + templates/map/client/libs/netgis/Toolbar.css | 145 ++ templates/map/client/libs/netgis/Toolbar.js | 447 +++++ templates/map/client/libs/netgis/Util.js | 161 ++ templates/map/client/netgis.min.css | 2 +- templates/map/client/netgis.min.js | 158 +- 45 files changed, 6234 insertions(+), 279 deletions(-) create mode 100644 templates/map/client/libs/netgis/Attribution.css create mode 100644 templates/map/client/libs/netgis/Attribution.js create mode 100644 templates/map/client/libs/netgis/Client.css create mode 100644 templates/map/client/libs/netgis/Client.js create mode 100644 templates/map/client/libs/netgis/Controls.css create mode 100644 templates/map/client/libs/netgis/Controls.js create mode 100644 templates/map/client/libs/netgis/Events.js create mode 100644 templates/map/client/libs/netgis/LayerTree.css create mode 100644 templates/map/client/libs/netgis/LayerTree.js create mode 100644 templates/map/client/libs/netgis/LayerTypes.js create mode 100644 templates/map/client/libs/netgis/Map.css create mode 100644 templates/map/client/libs/netgis/Map.js create mode 100644 templates/map/client/libs/netgis/MapOpenLayers.js create mode 100644 templates/map/client/libs/netgis/Menu.css create mode 100644 templates/map/client/libs/netgis/Menu.js create mode 100644 templates/map/client/libs/netgis/Modal.css create mode 100644 templates/map/client/libs/netgis/Modal.js create mode 100644 templates/map/client/libs/netgis/Modes.js create mode 100644 templates/map/client/libs/netgis/OWS.js create mode 100644 templates/map/client/libs/netgis/SLD.js create mode 100644 templates/map/client/libs/netgis/SearchParcel.css create mode 100644 templates/map/client/libs/netgis/SearchParcel.js create mode 100644 templates/map/client/libs/netgis/SearchPlace.js create mode 100644 templates/map/client/libs/netgis/Theme.css create mode 100644 templates/map/client/libs/netgis/Toolbar.css create mode 100644 templates/map/client/libs/netgis/Toolbar.js create mode 100644 templates/map/client/libs/netgis/Util.js diff --git a/konova/views/map_proxy.py b/konova/views/map_proxy.py index 13920fd3..5f3abc01 100644 --- a/konova/views/map_proxy.py +++ b/konova/views/map_proxy.py @@ -11,6 +11,8 @@ import requests from django.contrib.auth.decorators import login_required from django.http import JsonResponse, HttpRequest +from konova.sub_settings.proxy_settings import PROXIES + @login_required def map_client_proxy_view(request: HttpRequest): @@ -25,8 +27,11 @@ def map_client_proxy_view(request: HttpRequest): """ url = request.META.get("QUERY_STRING") - response = requests.get(url) - body = json.loads(response.content) + response = requests.get(url, proxies=PROXIES) + content = response.content + if isinstance(content, bytes): + content = content.decode("utf-8") + body = json.loads(content) if response.status_code != 200: return JsonResponse({ "status_code": response.status_code, diff --git a/templates/map/client/config.json b/templates/map/client/config.json index 1177cf15..f2907d67 100644 --- a/templates/map/client/config.json +++ b/templates/map/client/config.json @@ -84,6 +84,12 @@ { "url": "/client/proxy?https://www.geoportal.rlp.de/mapbender/geoportal/gaz_geom_mobile.php?outputFormat=json&resultTarget=web&searchEPSG={epsg}&maxResults=5&maxRows=5&featureClass=P&style=full&searchText={q}&name_startsWith={q}" }, + + "searchParcel": + { + "nameURL": "/client/proxy?https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_alkis/gem_search.php?placename={q}", + "parcelURL": "/client/proxy?https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/mod_alkis/flur_search.php?gmk_gmn={district}&fln={field}&fsn_zae={parcelA}&fsn_nen={parcelB}&export=json" + }, "export": { @@ -134,6 +140,13 @@ "stroke": "#0080ff", "strokeWidth": 3, "pointRadius": 6 + }, + + "parcel": + { + "fill": "rgba( 127, 255, 255, 0.5 )", + "stroke": "#7fffff", + "strokeWidth": 3 } } } \ No newline at end of file diff --git a/templates/map/client/index.html b/templates/map/client/index.html index 4612cd25..d9a9cd9e 100644 --- a/templates/map/client/index.html +++ b/templates/map/client/index.html @@ -4,7 +4,16 @@ - + + + + + + + + + +
@@ -23,8 +32,24 @@ - - + + + + + + + + + + + + + + + + + +