From fdf3adf5ae1b8a78b48eaa6cac1d7b61b0c0de52 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Sun, 14 Dec 2025 16:00:40 +0100 Subject: [PATCH 01/14] # Index views * refactors index view methods into classes * introduces AbstractIndexView as base class --- compensation/urls/compensation.py | 6 +- compensation/urls/eco_account.py | 6 +- .../views/compensation/compensation.py | 57 +++++++++--------- compensation/views/eco_account/eco_account.py | 54 ++++++++--------- ema/urls.py | 4 +- ema/views/ema.py | 49 ++++++++------- intervention/urls.py | 5 +- intervention/views/intervention.py | 60 +++++++++---------- konova/views/index.py | 23 +++++++ 9 files changed, 140 insertions(+), 124 deletions(-) create mode 100644 konova/views/index.py diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index 45a11594..c5c664cf 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -17,13 +17,13 @@ from compensation.views.compensation.action import NewCompensationActionView, Ed 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, \ - remove_view +from compensation.views.compensation.compensation import new_view, new_id_view, detail_view, edit_view, \ + remove_view, IndexCompensationView from compensation.views.compensation.log import CompensationLogView urlpatterns = [ # Main compensation - path("", index_view, name="index"), + path("", IndexCompensationView.as_view(), name="index"), path('new/id', new_id_view, name='new-id'), path('new/', new_view, name='new'), path('new', new_view, name='new'), diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index beaae8d9..e7fc85b2 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -8,8 +8,8 @@ Created on: 24.08.21 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.eco_account import new_view, new_id_view, edit_view, remove_view, \ + detail_view, IndexEcoAccountView 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 @@ -28,7 +28,7 @@ from compensation.views.eco_account.deduction import NewEcoAccountDeductionView, app_name = "acc" urlpatterns = [ - path("", index_view, name="index"), + path("", IndexEcoAccountView.as_view(), name="index"), path('new/', new_view, name='new'), path('new/id', new_id_view, name='new-id'), path('', detail_view, name='detail'), diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 15bac1f8..8cc050cd 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -8,8 +8,7 @@ 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.http import HttpRequest, JsonResponse, HttpResponse from django.shortcuts import get_object_or_404, render, redirect from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -28,38 +27,36 @@ 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, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \ COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE +from konova.views.index import AbstractIndexView -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ - Renders the index view for compensation +class IndexCompensationView(AbstractIndexView): + def get(self, request, *args, **kwargs) -> HttpResponse: + """ + 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 - ).order_by( - "-modified__timestamp" - ) - table = CompensationTable( - request=request, - queryset=compensations - ) - context = { - "table": table, - TAB_TITLE_IDENTIFIER: _("Compensations - Overview"), - } - context = BaseContext(request, context).context - return render(request, template, context) + Args: + request (HttpRequest): The incoming request + Returns: + A rendered view + """ + 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 + ).order_by( + "-modified__timestamp" + ) + table = CompensationTable( + request=request, + queryset=compensations + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: _("Compensations - Overview"), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) @login_required @default_group_required diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 28dbfc10..7fd7ff2f 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -7,8 +7,7 @@ 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.http import HttpRequest, JsonResponse, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -24,36 +23,35 @@ 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, \ IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE +from konova.views.index import AbstractIndexView -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ - Renders the index view for eco accounts +class IndexEcoAccountView(AbstractIndexView): + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """ + Renders the index view for eco accounts - Args: - request (HttpRequest): The incoming request + Args: + request (HttpRequest): The incoming request - Returns: - A rendered view - """ - template = "generic_index.html" - eco_accounts = EcoAccount.objects.filter( - deleted=None, - ).order_by( - "-modified__timestamp" - ) - 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) + Returns: + A rendered view + """ + eco_accounts = EcoAccount.objects.filter( + deleted=None, + ).order_by( + "-modified__timestamp" + ) + table = EcoAccountTable( + request=request, + queryset=eco_accounts + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) @login_required diff --git a/ema/urls.py b/ema/urls.py index bff7c41d..52cac31a 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -10,7 +10,7 @@ from django.urls import path 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 +from ema.views.ema import new_view, new_id_view, detail_view, edit_view, remove_view, IndexEmaView from ema.views.log import EmaLogView from ema.views.record import EmaRecordView from ema.views.report import report_view @@ -20,7 +20,7 @@ from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateVie app_name = "ema" urlpatterns = [ - path("", index_view, name="index"), + path("", IndexEmaView.as_view(), name="index"), path("new/", new_view, name="new"), path("new/id", new_id_view, name="new-id"), path("", detail_view, name="detail"), diff --git a/ema/views/ema.py b/ema/views/ema.py index 5322b5ef..cfcd0f6f 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -7,8 +7,7 @@ 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.http import HttpRequest, JsonResponse, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -25,35 +24,35 @@ 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, \ DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE +from konova.views.index import AbstractIndexView -@login_required -def index_view(request: HttpRequest): - """ Renders the index view for EMAs +class IndexEmaView(AbstractIndexView): + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """ Renders the index view for EMAs - Args: - request (HttpRequest): The incoming request + Args: + request (HttpRequest): The incoming request - Returns: + Returns: - """ - template = "generic_index.html" - emas = Ema.objects.filter( - deleted=None, - ).order_by( - "-modified__timestamp" - ) + """ + emas = Ema.objects.filter( + deleted=None, + ).order_by( + "-modified__timestamp" + ) - table = EmaTable( - request, - queryset=emas - ) - context = { - "table": table, - TAB_TITLE_IDENTIFIER: _("EMAs - Overview"), - } - context = BaseContext(request, context).context - return render(request, template, context) + table = EmaTable( + request, + queryset=emas + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: _("EMAs - Overview"), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) @login_required diff --git a/intervention/urls.py b/intervention/urls.py index 8a148197..27f13fb9 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -14,7 +14,8 @@ from intervention.views.deduction import NewInterventionDeductionView, EditInter 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 +from intervention.views.intervention import new_view, new_id_view, detail_view, edit_view, remove_view, \ + IndexInterventionView from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView from intervention.views.report import report_view @@ -25,7 +26,7 @@ from intervention.views.share import InterventionShareFormView, InterventionShar app_name = "intervention" urlpatterns = [ - path("", index_view, name="index"), + path("", IndexInterventionView.as_view(), name="index"), path('new/', new_view, name='new'), path('new/id', new_id_view, name='new-id'), path('', detail_view, name='detail'), diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index 0d6cc369..4cd2b019 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -7,7 +7,7 @@ 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.http import JsonResponse, HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, render, redirect from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -25,40 +25,38 @@ from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \ GEOMETRIES_IGNORED_TEMPLATE +from konova.views.index import AbstractIndexView -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ - Renders the index view for Interventions +class IndexInterventionView(AbstractIndexView): + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """ + Renders the index view for Interventions - Args: - request (HttpRequest): The incoming request + Args: + request (HttpRequest): The incoming request - Returns: - A rendered view - """ - template = "generic_index.html" - - # Filtering by user access is performed in table filter inside InterventionTableFilter class - interventions = Intervention.objects.filter( - deleted=None, # not deleted - ).select_related( - "legal" - ).order_by( - "-modified__timestamp" - ) - table = InterventionTable( - request=request, - queryset=interventions - ) - context = { - "table": table, - TAB_TITLE_IDENTIFIER: _("Interventions - Overview"), - } - context = BaseContext(request, context).context - return render(request, template, context) + Returns: + A rendered view + """ + # Filtering by user access is performed in table filter inside InterventionTableFilter class + interventions = Intervention.objects.filter( + deleted=None, # not deleted + ).select_related( + "legal" + ).order_by( + "-modified__timestamp" + ) + table = InterventionTable( + request=request, + queryset=interventions + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: _("Interventions - Overview"), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) @login_required diff --git a/konova/views/index.py b/konova/views/index.py new file mode 100644 index 00000000..cafaeff9 --- /dev/null +++ b/konova/views/index.py @@ -0,0 +1,23 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin +from django.utils.decorators import method_decorator +from django.views import View + +from konova.decorators import any_group_check + + +class AbstractIndexView(LoginRequiredMixin, View): + _TEMPLATE = "generic_index.html" + + class Meta: + abstract = True + + @method_decorator(login_required) + @method_decorator(any_group_check) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) -- 2.49.1 From 72914bab9dd05c0d8af6b612d698b8ec9e5a6218 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Sun, 14 Dec 2025 16:11:50 +0100 Subject: [PATCH 02/14] # Detail View * refactors detail view methods into classes * introduces AbstractDetailView --- compensation/urls/compensation.py | 5 +- compensation/urls/eco_account.py | 5 +- .../views/compensation/compensation.py | 79 ------------ compensation/views/compensation/detail.py | 97 ++++++++++++++ compensation/views/eco_account/detail.py | 97 ++++++++++++++ ema/urls.py | 5 +- ema/views/detail.py | 76 +++++++++++ ema/views/ema.py | 60 --------- intervention/urls.py | 6 +- intervention/views/intervention.py | 121 ++++++++---------- konova/views/detail.py | 23 ++++ konova/views/index.py | 2 - 12 files changed, 360 insertions(+), 216 deletions(-) create mode 100644 compensation/views/compensation/detail.py create mode 100644 compensation/views/eco_account/detail.py create mode 100644 ema/views/detail.py create mode 100644 konova/views/detail.py diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index c5c664cf..ca6029b5 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -7,6 +7,7 @@ Created on: 24.08.21 """ from django.urls import path +from compensation.views.compensation.detail import DetailCompensationView from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \ GetCompensationDocumentView, RemoveCompensationDocumentView from compensation.views.compensation.resubmission import CompensationResubmissionView @@ -17,7 +18,7 @@ from compensation.views.compensation.action import NewCompensationActionView, Ed RemoveCompensationActionView from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \ RemoveCompensationStateView -from compensation.views.compensation.compensation import new_view, new_id_view, detail_view, edit_view, \ +from compensation.views.compensation.compensation import new_view, new_id_view, edit_view, \ remove_view, IndexCompensationView from compensation.views.compensation.log import CompensationLogView @@ -27,7 +28,7 @@ urlpatterns = [ path('new/id', new_id_view, name='new-id'), path('new/', new_view, name='new'), path('new', new_view, name='new'), - path('', detail_view, name='detail'), + path('', DetailCompensationView.as_view(), name='detail'), 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 e7fc85b2..8c8a27fd 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -8,8 +8,9 @@ Created on: 24.08.21 from django.urls import path from compensation.autocomplete.eco_account import EcoAccountAutocomplete +from compensation.views.eco_account.detail import DetailEcoAccountView from compensation.views.eco_account.eco_account import new_view, new_id_view, edit_view, remove_view, \ - detail_view, IndexEcoAccountView + IndexEcoAccountView 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 @@ -31,7 +32,7 @@ urlpatterns = [ path("", IndexEcoAccountView.as_view(), name="index"), path('new/', new_view, name='new'), path('new/id', new_id_view, name='new-id'), - path('', detail_view, name='detail'), + path('', DetailEcoAccountView.as_view(), name='detail'), path('/log', EcoAccountLogView.as_view(), name='log'), path('/record', EcoAccountRecordView.as_view(), name='record'), path('/report', report_view, name='report'), diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 8cc050cd..bd669672 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -211,85 +211,6 @@ def edit_view(request: HttpRequest, id: str): context = BaseContext(request, context).context return render(request, template, context) - -@login_required -@any_group_check -@uuid_required -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.objects.select_related( - "modified", - "created", - "geometry" - ), - id=id, - deleted=None, - intervention__deleted=None, - ) - 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 = comp.get_surface_before_states() - sum_after_states = comp.get_surface_after_states() - 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) - - requesting_user_is_only_shared_user = comp.is_only_shared_with(_user) - if requesting_user_is_only_shared_user: - messages.info( - request, - DO_NOT_FORGET_TO_SHARE - ) - - context = { - "obj": comp, - "last_checked": last_checked, - "last_checked_tooltip": last_checked_tooltip, - "geom_form": geom_form, - "parcels": parcels, - "is_entry_shared": 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": _user.in_group(DEFAULT_GROUP), - "is_zb_member": _user.in_group(ZB_GROUP), - "is_ets_member": _user.in_group(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_modal @login_required @default_group_required diff --git a/compensation/views/compensation/detail.py b/compensation/views/compensation/detail.py new file mode 100644 index 00000000..6b342119 --- /dev/null +++ b/compensation/views/compensation/detail.py @@ -0,0 +1,97 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +from django.contrib import messages +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render, get_object_or_404 + +from compensation.models import Compensation +from konova.contexts import BaseContext +from konova.forms import SimpleGeomForm +from konova.settings import ETS_GROUP, ZB_GROUP, DEFAULT_GROUP +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE, DATA_CHECKED_PREVIOUSLY_TEMPLATE +from konova.views.detail import AbstractDetailView + + +class DetailCompensationView(AbstractDetailView): + _TEMPLATE = "compensation/detail/compensation/view.html" + + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + + """ Renders a detail view for a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + + Returns: + + """ + comp = get_object_or_404( + Compensation.objects.select_related( + "modified", + "created", + "geometry" + ), + id=id, + deleted=None, + intervention__deleted=None, + ) + 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 = comp.get_surface_before_states() + sum_after_states = comp.get_surface_after_states() + 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 + ) + + requesting_user_is_only_shared_user = comp.is_only_shared_with(_user) + if requesting_user_is_only_shared_user: + messages.info( + request, + DO_NOT_FORGET_TO_SHARE + ) + + context = { + "obj": comp, + "last_checked": last_checked, + "last_checked_tooltip": last_checked_tooltip, + "geom_form": geom_form, + "parcels": parcels, + "is_entry_shared": 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": _user.in_group(DEFAULT_GROUP), + "is_zb_member": _user.in_group(ZB_GROUP), + "is_ets_member": _user.in_group(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, self._TEMPLATE, context) \ No newline at end of file diff --git a/compensation/views/eco_account/detail.py b/compensation/views/eco_account/detail.py new file mode 100644 index 00000000..500940a9 --- /dev/null +++ b/compensation/views/eco_account/detail.py @@ -0,0 +1,97 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +from django.contrib import messages +from django.http import HttpRequest, HttpResponse +from django.shortcuts import render, get_object_or_404 + +from compensation.models import EcoAccount +from konova.contexts import BaseContext +from konova.forms import SimpleGeomForm +from konova.settings import ETS_GROUP, ZB_GROUP, DEFAULT_GROUP +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE +from konova.views.detail import AbstractDetailView + + +class DetailEcoAccountView(AbstractDetailView): + _TEMPLATE = "compensation/detail/eco_account/view.html" + + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders a detail view for a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + + Returns: + + """ + acc = get_object_or_404( + EcoAccount.objects.prefetch_related( + "deadlines", + ).select_related( + 'geometry', + 'responsible', + ), + id=id, + deleted=None, + ) + 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 = acc.get_surface_before_states() + sum_after_states = acc.get_surface_after_states() + diff_states = abs(sum_before_states - sum_after_states) + # Calculate rest of available surface for deductions + available_total = acc.deductable_rest + available_relative = acc.get_deductable_rest_relative() + + # 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) + + requesting_user_is_only_shared_user = acc.is_only_shared_with(_user) + if requesting_user_is_only_shared_user: + messages.info( + request, + DO_NOT_FORGET_TO_SHARE + ) + + context = { + "obj": acc, + "geom_form": geom_form, + "parcels": parcels, + "is_entry_shared": 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": _user.in_group(DEFAULT_GROUP), + "is_zb_member": _user.in_group(ZB_GROUP), + "is_ets_member": _user.in_group(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, self._TEMPLATE, context) \ No newline at end of file diff --git a/ema/urls.py b/ema/urls.py index 52cac31a..0c4308b5 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -9,8 +9,9 @@ from django.urls import path from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView +from ema.views.detail import DetailEmaView from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView -from ema.views.ema import new_view, new_id_view, detail_view, edit_view, remove_view, IndexEmaView +from ema.views.ema import new_view, new_id_view, edit_view, remove_view, IndexEmaView from ema.views.log import EmaLogView from ema.views.record import EmaRecordView from ema.views.report import report_view @@ -23,7 +24,7 @@ urlpatterns = [ path("", IndexEmaView.as_view(), name="index"), path("new/", new_view, name="new"), path("new/id", new_id_view, name="new-id"), - path("", detail_view, name="detail"), + path("", DetailEmaView.as_view(), name="detail"), path('/log', EmaLogView.as_view(), name='log'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), diff --git a/ema/views/detail.py b/ema/views/detail.py new file mode 100644 index 00000000..16d10125 --- /dev/null +++ b/ema/views/detail.py @@ -0,0 +1,76 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +from django.contrib import messages +from django.http import HttpResponse, HttpRequest +from django.shortcuts import get_object_or_404, render + +from ema.models import Ema +from konova.contexts import BaseContext +from konova.forms import SimpleGeomForm +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 DO_NOT_FORGET_TO_SHARE +from konova.views.detail import AbstractDetailView + + +class DetailEmaView(AbstractDetailView): + _TEMPLATE = "ema/detail/view.html" + + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders the detail view of an EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id, deleted=None) + + geom_form = SimpleGeomForm(instance=ema) + parcels = ema.get_underlying_parcels() + _user = request.user + is_entry_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 = ema.get_surface_before_states() + sum_after_states = ema.get_surface_after_states() + diff_states = abs(sum_before_states - sum_after_states) + + ema.set_status_messages(request) + + requesting_user_is_only_shared_user = ema.is_only_shared_with(_user) + if requesting_user_is_only_shared_user: + messages.info( + request, + DO_NOT_FORGET_TO_SHARE + ) + + context = { + "obj": ema, + "geom_form": geom_form, + "parcels": parcels, + "is_entry_shared": is_entry_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": _user.in_group(DEFAULT_GROUP), + "is_zb_member": _user.in_group(ZB_GROUP), + "is_ets_member": _user.in_group(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, self._TEMPLATE, context) diff --git a/ema/views/ema.py b/ema/views/ema.py index cfcd0f6f..1ea59120 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -129,66 +129,6 @@ def new_id_view(request: HttpRequest): ) -@login_required -@uuid_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_entry_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 = ema.get_surface_before_states() - sum_after_states = ema.get_surface_after_states() - diff_states = abs(sum_before_states - sum_after_states) - - ema.set_status_messages(request) - - requesting_user_is_only_shared_user = ema.is_only_shared_with(_user) - if requesting_user_is_only_shared_user: - messages.info( - request, - DO_NOT_FORGET_TO_SHARE - ) - - context = { - "obj": ema, - "geom_form": geom_form, - "parcels": parcels, - "is_entry_shared": is_entry_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": _user.in_group(DEFAULT_GROUP), - "is_zb_member": _user.in_group(ZB_GROUP), - "is_ets_member": _user.in_group(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") diff --git a/intervention/urls.py b/intervention/urls.py index 27f13fb9..136057dc 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -14,8 +14,8 @@ from intervention.views.deduction import NewInterventionDeductionView, EditInter RemoveInterventionDeductionView from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \ RemoveInterventionDocumentView, EditInterventionDocumentView -from intervention.views.intervention import new_view, new_id_view, detail_view, edit_view, remove_view, \ - IndexInterventionView +from intervention.views.intervention import new_view, new_id_view, edit_view, remove_view, \ + IndexInterventionView, DetailInterventionView from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView from intervention.views.report import report_view @@ -29,7 +29,7 @@ urlpatterns = [ path("", IndexInterventionView.as_view(), name="index"), path('new/', new_view, name='new'), path('new/id', new_id_view, name='new-id'), - path('', detail_view, name='detail'), + path('', DetailInterventionView.as_view(), name='detail'), path('/log', InterventionLogView.as_view(), name='log'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index 4cd2b019..4d8e44ce 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -25,6 +25,7 @@ from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \ GEOMETRIES_IGNORED_TEMPLATE +from konova.views.detail import AbstractDetailView from konova.views.index import AbstractIndexView @@ -134,79 +135,67 @@ def new_id_view(request: HttpRequest): ) -@login_required -@any_group_check -@uuid_required -def detail_view(request: HttpRequest, id: str): - """ Renders a detail view for viewing an intervention's data +class DetailInterventionView(AbstractDetailView): + _TEMPLATE = "intervention/detail/view.html" - 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", - ).prefetch_related( - "legal__revocations", - ), - id=id, - deleted=None - ) - 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 + def get(self, request, id: str, *args, **kwargs) -> HttpResponse: + # Fetch data, filter out deleted related data + intervention = get_object_or_404( + Intervention.objects.select_related( + "geometry", + "legal", + "responsible", + ).prefetch_related( + "legal__revocations", + ), + id=id, + deleted=None ) - - has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists() - - requesting_user_is_only_shared_user = intervention.is_only_shared_with(_user) - if requesting_user_is_only_shared_user: - messages.info( - request, - DO_NOT_FORGET_TO_SHARE + compensations = intervention.compensations.filter( + deleted=None, ) + _user = request.user + is_data_shared = intervention.is_shared_with(user=_user) - context = { - "obj": intervention, - "last_checked": last_checked, - "last_checked_tooltip": last_checked_tooltip, - "compensations": compensations, - "is_entry_shared": is_data_shared, - "geom_form": geom_form, - "is_default_member": _user.in_group(DEFAULT_GROUP), - "is_zb_member": _user.in_group(ZB_GROUP), - "is_ets_member": _user.in_group(ETS_GROUP), - "LANIS_LINK": intervention.get_LANIS_link(), - "has_payment_without_document": has_payment_without_document, - TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}", - } + 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 + ) - request = intervention.set_status_messages(request) + has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists() - context = BaseContext(request, context).context - return render(request, template, context) + requesting_user_is_only_shared_user = intervention.is_only_shared_with(_user) + if requesting_user_is_only_shared_user: + messages.info( + request, + DO_NOT_FORGET_TO_SHARE + ) + context = { + "obj": intervention, + "last_checked": last_checked, + "last_checked_tooltip": last_checked_tooltip, + "compensations": compensations, + "is_entry_shared": is_data_shared, + "geom_form": geom_form, + "is_default_member": _user.in_group(DEFAULT_GROUP), + "is_zb_member": _user.in_group(ZB_GROUP), + "is_ets_member": _user.in_group(ETS_GROUP), + "LANIS_LINK": intervention.get_LANIS_link(), + "has_payment_without_document": has_payment_without_document, + TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}", + } + + request = intervention.set_status_messages(request) + + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) @login_required @default_group_required diff --git a/konova/views/detail.py b/konova/views/detail.py new file mode 100644 index 00000000..3207680f --- /dev/null +++ b/konova/views/detail.py @@ -0,0 +1,23 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +from django.contrib.auth.mixins import LoginRequiredMixin +from django.utils.decorators import method_decorator +from django.views import View + +from konova.decorators import uuid_required, any_group_check + + +class AbstractDetailView(LoginRequiredMixin, View): + _TEMPLATE = None + + class Meta: + abstract = True + + @method_decorator(uuid_required) + @method_decorator(any_group_check) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) + diff --git a/konova/views/index.py b/konova/views/index.py index cafaeff9..7c1298fd 100644 --- a/konova/views/index.py +++ b/konova/views/index.py @@ -3,7 +3,6 @@ Author: Michel Peltriaux Created on: 14.12.25 """ -from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.utils.decorators import method_decorator from django.views import View @@ -17,7 +16,6 @@ class AbstractIndexView(LoginRequiredMixin, View): class Meta: abstract = True - @method_decorator(login_required) @method_decorator(any_group_check) def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) -- 2.49.1 From 2da6f1dc6f7b02ee32a7c926f1f894faf21fc589 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Sun, 14 Dec 2025 16:25:49 +0100 Subject: [PATCH 03/14] # Identifier Generator View * refactors identifier generator view methods into classes * introduces IdentifierGenerator * introduces AbstractIdentifierGeneratorView --- compensation/urls/compensation.py | 6 +- compensation/urls/eco_account.py | 6 +- .../views/compensation/compensation.py | 30 ++--- compensation/views/eco_account/eco_account.py | 113 ++---------------- ema/urls.py | 4 +- ema/views/ema.py | 30 ++--- intervention/urls.py | 7 +- intervention/views/detail.py | 79 ++++++++++++ intervention/views/intervention.py | 94 ++------------- konova/utils/generators.py | 23 ++++ konova/views/identifier.py | 32 +++++ 11 files changed, 176 insertions(+), 248 deletions(-) create mode 100644 intervention/views/detail.py create mode 100644 konova/views/identifier.py diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index ca6029b5..1f171f53 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -18,14 +18,14 @@ from compensation.views.compensation.action import NewCompensationActionView, Ed RemoveCompensationActionView from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \ RemoveCompensationStateView -from compensation.views.compensation.compensation import new_view, new_id_view, edit_view, \ - remove_view, IndexCompensationView +from compensation.views.compensation.compensation import new_view, edit_view, \ + remove_view, IndexCompensationView, CompensationIdentifierGeneratorView from compensation.views.compensation.log import CompensationLogView urlpatterns = [ # Main compensation path("", IndexCompensationView.as_view(), name="index"), - path('new/id', new_id_view, name='new-id'), + path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'), path('new/', new_view, name='new'), path('new', new_view, name='new'), path('', DetailCompensationView.as_view(), name='detail'), diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index 8c8a27fd..9b0263c8 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -9,8 +9,8 @@ from django.urls import path from compensation.autocomplete.eco_account import EcoAccountAutocomplete from compensation.views.eco_account.detail import DetailEcoAccountView -from compensation.views.eco_account.eco_account import new_view, new_id_view, edit_view, remove_view, \ - IndexEcoAccountView +from compensation.views.eco_account.eco_account import new_view, edit_view, remove_view, \ + IndexEcoAccountView, EcoAccountIdentifierGeneratorView 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 @@ -31,7 +31,7 @@ app_name = "acc" urlpatterns = [ path("", IndexEcoAccountView.as_view(), name="index"), path('new/', new_view, name='new'), - path('new/id', new_id_view, name='new-id'), + path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'), path('', DetailEcoAccountView.as_view(), name='detail'), path('/log', EcoAccountLogView.as_view(), name='log'), path('/record', EcoAccountRecordView.as_view(), name='record'), diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index bd669672..94fce5f9 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -8,7 +8,7 @@ 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.http import HttpRequest, JsonResponse, HttpResponse +from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, render, redirect from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -18,15 +18,14 @@ 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, login_required_modal, \ - uuid_required +from konova.decorators import shared_access_required, default_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 from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ +from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, \ RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \ - COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE + COMPENSATION_ADDED_TEMPLATE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE +from konova.views.identifier import AbstractIdentifierGeneratorView from konova.views.index import AbstractIndexView @@ -128,23 +127,8 @@ def new_view(request: HttpRequest, intervention_id: str = None): 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 - } - ) +class CompensationIdentifierGeneratorView(AbstractIdentifierGeneratorView): + _MODEL = Compensation @login_required diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 7fd7ff2f..3acd5714 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -7,7 +7,7 @@ Created on: 19.08.22 """ from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.http import HttpRequest, JsonResponse, HttpResponse +from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -16,13 +16,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, login_required_modal, \ - uuid_required +from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.forms import SimpleGeomForm -from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP +from konova.settings import ETS_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, \ - IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE + IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE +from konova.views.identifier import AbstractIdentifierGeneratorView from konova.views.index import AbstractIndexView @@ -109,25 +109,8 @@ def new_view(request: HttpRequest): 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 - } - ) - +class EcoAccountIdentifierGeneratorView(AbstractIdentifierGeneratorView): + _MODEL = EcoAccount @login_required @default_group_required @@ -190,88 +173,6 @@ def edit_view(request: HttpRequest, id: str): return render(request, template, context) -@login_required -@any_group_check -@uuid_required -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, - deleted=None, - ) - 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 = acc.get_surface_before_states() - sum_after_states = acc.get_surface_after_states() - diff_states = abs(sum_before_states - sum_after_states) - # Calculate rest of available surface for deductions - available_total = acc.deductable_rest - available_relative = acc.get_deductable_rest_relative() - - # 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) - - requesting_user_is_only_shared_user = acc.is_only_shared_with(_user) - if requesting_user_is_only_shared_user: - messages.info( - request, - DO_NOT_FORGET_TO_SHARE - ) - - context = { - "obj": acc, - "geom_form": geom_form, - "parcels": parcels, - "is_entry_shared": 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": _user.in_group(DEFAULT_GROUP), - "is_zb_member": _user.in_group(ZB_GROUP), - "is_ets_member": _user.in_group(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_modal @login_required @default_group_required diff --git a/ema/urls.py b/ema/urls.py index 0c4308b5..5e20b14c 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -11,7 +11,7 @@ from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActio from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView from ema.views.detail import DetailEmaView from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView -from ema.views.ema import new_view, new_id_view, edit_view, remove_view, IndexEmaView +from ema.views.ema import new_view, edit_view, remove_view, IndexEmaView, EmaIdentifierGeneratorView from ema.views.log import EmaLogView from ema.views.record import EmaRecordView from ema.views.report import report_view @@ -23,7 +23,7 @@ app_name = "ema" urlpatterns = [ path("", IndexEmaView.as_view(), name="index"), path("new/", new_view, name="new"), - path("new/id", new_id_view, name="new-id"), + path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"), path("", DetailEmaView.as_view(), name="detail"), path('/log', EmaLogView.as_view(), name='log'), path('/edit', edit_view, name='edit'), diff --git a/ema/views/ema.py b/ema/views/ema.py index 1ea59120..9c0a216f 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -10,20 +10,20 @@ from django.contrib.auth.decorators import login_required from django.http import HttpRequest, JsonResponse, HttpResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse +from django.utils.decorators import method_decorator 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, login_required_modal, \ - uuid_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 from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \ - DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE + GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE +from konova.views.identifier import AbstractIdentifierGeneratorView from konova.views.index import AbstractIndexView @@ -110,24 +110,12 @@ def new_view(request: HttpRequest): 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 - } - ) +class EmaIdentifierGeneratorView(AbstractIdentifierGeneratorView): + _MODEL = Ema + @method_decorator(conservation_office_group_required) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) @login_required @conservation_office_group_required diff --git a/intervention/urls.py b/intervention/urls.py index 136057dc..f9249fa9 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -14,8 +14,9 @@ from intervention.views.deduction import NewInterventionDeductionView, EditInter RemoveInterventionDeductionView from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \ RemoveInterventionDocumentView, EditInterventionDocumentView -from intervention.views.intervention import new_view, new_id_view, edit_view, remove_view, \ - IndexInterventionView, DetailInterventionView +from intervention.views.intervention import new_view, edit_view, remove_view, \ + IndexInterventionView, InterventionIdentifierGeneratorView +from intervention.views.detail import DetailInterventionView from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView from intervention.views.report import report_view @@ -28,7 +29,7 @@ app_name = "intervention" urlpatterns = [ path("", IndexInterventionView.as_view(), name="index"), path('new/', new_view, name='new'), - path('new/id', new_id_view, name='new-id'), + path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'), path('', DetailInterventionView.as_view(), name='detail'), path('/log', InterventionLogView.as_view(), name='log'), path('/edit', edit_view, name='edit'), diff --git a/intervention/views/detail.py b/intervention/views/detail.py new file mode 100644 index 00000000..68f98708 --- /dev/null +++ b/intervention/views/detail.py @@ -0,0 +1,79 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +from django.contrib import messages +from django.http import HttpResponse +from django.shortcuts import get_object_or_404, render + +from intervention.models import Intervention +from konova.contexts import BaseContext +from konova.forms import SimpleGeomForm +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, DO_NOT_FORGET_TO_SHARE +from konova.views.detail import AbstractDetailView + + +class DetailInterventionView(AbstractDetailView): + _TEMPLATE = "intervention/detail/view.html" + + def get(self, request, id: str, *args, **kwargs) -> HttpResponse: + # Fetch data, filter out deleted related data + intervention = get_object_or_404( + Intervention.objects.select_related( + "geometry", + "legal", + "responsible", + ).prefetch_related( + "legal__revocations", + ), + id=id, + deleted=None + ) + 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 + ) + + has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists() + + requesting_user_is_only_shared_user = intervention.is_only_shared_with(_user) + if requesting_user_is_only_shared_user: + messages.info( + request, + DO_NOT_FORGET_TO_SHARE + ) + + context = { + "obj": intervention, + "last_checked": last_checked, + "last_checked_tooltip": last_checked_tooltip, + "compensations": compensations, + "is_entry_shared": is_data_shared, + "geom_form": geom_form, + "is_default_member": _user.in_group(DEFAULT_GROUP), + "is_zb_member": _user.in_group(ZB_GROUP), + "is_ets_member": _user.in_group(ETS_GROUP), + "LANIS_LINK": intervention.get_LANIS_link(), + "has_payment_without_document": has_payment_without_document, + TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}", + } + + request = intervention.set_status_messages(request) + + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index 4d8e44ce..24d25983 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -7,7 +7,7 @@ Created on: 19.08.22 """ from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.http import JsonResponse, HttpRequest, HttpResponse +from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, render, redirect from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -16,16 +16,14 @@ 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, login_required_modal, \ - uuid_required +from konova.decorators import default_group_required, shared_access_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 from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ - CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \ +from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, \ + CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, \ GEOMETRIES_IGNORED_TEMPLATE -from konova.views.detail import AbstractDetailView +from konova.views.identifier import AbstractIdentifierGeneratorView from konova.views.index import AbstractIndexView @@ -115,87 +113,9 @@ def new_view(request: HttpRequest): context = BaseContext(request, context).context return render(request, template, context) +class InterventionIdentifierGeneratorView(AbstractIdentifierGeneratorView): + _MODEL = Intervention -@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 - } - ) - - -class DetailInterventionView(AbstractDetailView): - _TEMPLATE = "intervention/detail/view.html" - - def get(self, request, id: str, *args, **kwargs) -> HttpResponse: - # Fetch data, filter out deleted related data - intervention = get_object_or_404( - Intervention.objects.select_related( - "geometry", - "legal", - "responsible", - ).prefetch_related( - "legal__revocations", - ), - id=id, - deleted=None - ) - 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 - ) - - has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists() - - requesting_user_is_only_shared_user = intervention.is_only_shared_with(_user) - if requesting_user_is_only_shared_user: - messages.info( - request, - DO_NOT_FORGET_TO_SHARE - ) - - context = { - "obj": intervention, - "last_checked": last_checked, - "last_checked_tooltip": last_checked_tooltip, - "compensations": compensations, - "is_entry_shared": is_data_shared, - "geom_form": geom_form, - "is_default_member": _user.in_group(DEFAULT_GROUP), - "is_zb_member": _user.in_group(ZB_GROUP), - "is_ets_member": _user.in_group(ETS_GROUP), - "LANIS_LINK": intervention.get_LANIS_link(), - "has_payment_without_document": has_payment_without_document, - TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}", - } - - request = intervention.set_status_messages(request) - - context = BaseContext(request, context).context - return render(request, self._TEMPLATE, context) @login_required @default_group_required diff --git a/konova/utils/generators.py b/konova/utils/generators.py index 78e075ad..d21b0437 100644 --- a/konova/utils/generators.py +++ b/konova/utils/generators.py @@ -62,3 +62,26 @@ def generate_qr_code(content: str, size: int = 20) -> str: stream = BytesIO() qrcode_img.save(stream) return stream.getvalue().decode() + + +class IdentifierGenerator: + _MODEL = None + + def __init__(self, model): + from konova.models import BaseObject + if not issubclass(model, BaseObject): + raise AssertionError("Model must be a subclass of BaseObject!") + + self._MODEL = model + + def generate_id(self) -> str: + """ Generates a unique identifier + + Returns: + + """ + unpersisted_object = self._MODEL() + identifier = unpersisted_object.generate_new_identifier() + while self._MODEL.objects.filter(identifier=identifier).exists(): + identifier = unpersisted_object.generate_new_identifier() + return identifier diff --git a/konova/views/identifier.py b/konova/views/identifier.py new file mode 100644 index 00000000..b5fdd4be --- /dev/null +++ b/konova/views/identifier.py @@ -0,0 +1,32 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest, JsonResponse +from django.utils.decorators import method_decorator +from django.views import View + +from konova.decorators import default_group_required +from konova.utils.generators import IdentifierGenerator + + +class AbstractIdentifierGeneratorView(LoginRequiredMixin, View): + _MODEL = None + + class Meta: + abstract = True + + @method_decorator(default_group_required) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) + + def get(self, request: HttpRequest, *args, **kwargs): + generator = IdentifierGenerator(model=self._MODEL) + identifier = generator.generate_id() + return JsonResponse( + data={ + "gen_data": identifier + } + ) \ No newline at end of file -- 2.49.1 From e4c459f92e54d61ecd2e9a8c60944ce4a598df89 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Sun, 14 Dec 2025 16:35:58 +0100 Subject: [PATCH 04/14] # Public report * refactors public report view methods into classes * introduces AbstractPublicReportView --- compensation/urls/compensation.py | 4 +- compensation/urls/eco_account.py | 4 +- compensation/views/compensation/report.py | 112 +++++++++---------- compensation/views/eco_account/report.py | 125 +++++++++++----------- ema/urls.py | 4 +- ema/views/report.py | 112 +++++++++---------- intervention/urls.py | 4 +- intervention/views/report.py | 101 ++++++++--------- konova/views/detail.py | 3 + konova/views/index.py | 4 + konova/views/report.py | 28 +++++ 11 files changed, 268 insertions(+), 233 deletions(-) create mode 100644 konova/views/report.py diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index 1f171f53..c77faed1 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -11,7 +11,7 @@ from compensation.views.compensation.detail import DetailCompensationView 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.report import CompensationPublicReportView from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \ RemoveCompensationDeadlineView from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \ @@ -44,7 +44,7 @@ urlpatterns = [ 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('/report', CompensationPublicReportView.as_view(), name='report'), path('/resub', CompensationResubmissionView.as_view(), name='resubmission-create'), # Documents diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index 9b0263c8..de99a3f5 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 new_view, edit_view, remo IndexEcoAccountView, EcoAccountIdentifierGeneratorView 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.report import EcoAccountPublicReportView from compensation.views.eco_account.resubmission import EcoAccountResubmissionView from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \ RemoveEcoAccountStateView @@ -35,7 +35,7 @@ urlpatterns = [ path('', DetailEcoAccountView.as_view(), name='detail'), path('/log', EcoAccountLogView.as_view(), name='log'), path('/record', EcoAccountRecordView.as_view(), name='record'), - path('/report', report_view, name='report'), + path('/report', EcoAccountPublicReportView.as_view(), name='report'), path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), path('/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'), diff --git a/compensation/views/compensation/report.py b/compensation/views/compensation/report.py index 96081627..077367dd 100644 --- a/compensation/views/compensation/report.py +++ b/compensation/views/compensation/report.py @@ -5,77 +5,77 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.http import HttpRequest +from django.http import HttpRequest, HttpResponse 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.decorators import uuid_required from konova.forms import SimpleGeomForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.generators import generate_qr_code +from konova.views.report import AbstractPublicReportView -@uuid_required -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 +class CompensationPublicReportView(AbstractPublicReportView): + _TEMPLATE = "compensation/report/compensation/report.html" - Returns: + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders the public report view - """ - # Reuse the compensation report template since compensations are structurally identical - template = "compensation/report/compensation/report.html" - comp = get_object_or_404(Compensation, id=id) + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention + + Returns: + + """ + 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") - 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 = { + "obj": comp, + "qrcode": { + "img": qrcode_img, + "url": qrcode_url, + }, + "qrcode_lanis": { + "img": qrcode_img_lanis, + "url": qrcode_lanis_url, + }, + "is_entry_shared": False, # disables action buttons during rendering + "before_states": before_states, + "after_states": after_states, + "geom_form": geom_form, + "parcels": parcels, + "actions": actions, + "tables_scrollable": False, 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, - }, - "is_entry_shared": False, # disables action buttons during rendering - "before_states": before_states, - "after_states": after_states, - "geom_form": geom_form, - "parcels": parcels, - "actions": actions, - "tables_scrollable": False, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) + return render(request, self._TEMPLATE, context) diff --git a/compensation/views/eco_account/report.py b/compensation/views/eco_account/report.py index f61a7bfc..494c4c8e 100644 --- a/compensation/views/eco_account/report.py +++ b/compensation/views/eco_account/report.py @@ -5,85 +5,84 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.http import HttpRequest +from django.http import HttpRequest, HttpResponse 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.decorators import uuid_required from konova.forms import SimpleGeomForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.generators import generate_qr_code +from konova.views.report import AbstractPublicReportView -@uuid_required -def report_view(request: HttpRequest, id: str): - """ Renders the public report view +class EcoAccountPublicReportView(AbstractPublicReportView): + _TEMPLATE = "compensation/report/eco_account/report.html" - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders the public report view - Returns: + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention - """ - # 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) + Returns: + + """ + 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) - 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 = { + "obj": acc, + "qrcode": { + "img": qrcode_img, + "url": qrcode_url, + }, + "qrcode_lanis": { + "img": qrcode_img_lanis, + "url": qrcode_lanis_url, + }, + "is_entry_shared": False, # disables action buttons during rendering + "before_states": before_states, + "after_states": after_states, + "geom_form": geom_form, + "parcels": parcels, + "actions": actions, + "deductions": deductions, + "tables_scrollable": False, 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, - }, - "is_entry_shared": False, # disables action buttons during rendering - "before_states": before_states, - "after_states": after_states, - "geom_form": geom_form, - "parcels": parcels, - "actions": actions, - "deductions": deductions, - "tables_scrollable": False, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) + return render(request, self._TEMPLATE, context) diff --git a/ema/urls.py b/ema/urls.py index 5e20b14c..705808bc 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -14,7 +14,7 @@ from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEm from ema.views.ema import new_view, edit_view, remove_view, IndexEmaView, EmaIdentifierGeneratorView from ema.views.log import EmaLogView from ema.views.record import EmaRecordView -from ema.views.report import report_view +from ema.views.report import EmaPublicReportView from ema.views.resubmission import EmaResubmissionView from ema.views.share import EmaShareFormView, EmaShareByTokenView from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView @@ -29,7 +29,7 @@ urlpatterns = [ path('/edit', edit_view, name='edit'), path('/remove', remove_view, name='remove'), path('/record', EmaRecordView.as_view(), name='record'), - path('/report', report_view, name='report'), + path('/report', EmaPublicReportView.as_view(), name='report'), path('/resub', EmaResubmissionView.as_view(), name='resubmission-create'), path('/state/new', NewEmaStateView.as_view(), name='new-state'), diff --git a/ema/views/report.py b/ema/views/report.py index 93af6211..e9c54f13 100644 --- a/ema/views/report.py +++ b/ema/views/report.py @@ -5,77 +5,77 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.http import HttpRequest +from django.http import HttpRequest, HttpResponse 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.decorators import uuid_required from konova.forms import SimpleGeomForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.generators import generate_qr_code +from konova.views.report import AbstractPublicReportView -@uuid_required -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 +class EmaPublicReportView(AbstractPublicReportView): + _TEMPLATE = "ema/report/report.html" - Returns: + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders the public report view - """ - # Reuse the compensation report template since EMAs are structurally identical - template = "ema/report/report.html" - ema = get_object_or_404(Ema, id=id) + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention + + Returns: + + """ + 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") - 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 = { + "obj": ema, + "qrcode": { + "img": qrcode_img, + "url": qrcode_url + }, + "qrcode_lanis": { + "img": qrcode_img_lanis, + "url": qrcode_lanis_url + }, + "is_entry_shared": False, # disables action buttons during rendering + "before_states": before_states, + "after_states": after_states, + "geom_form": geom_form, + "parcels": parcels, + "actions": actions, + "tables_scrollable": False, 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 - }, - "is_entry_shared": False, # disables action buttons during rendering - "before_states": before_states, - "after_states": after_states, - "geom_form": geom_form, - "parcels": parcels, - "actions": actions, - "tables_scrollable": False, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) + return render(request, self._TEMPLATE, context) \ No newline at end of file diff --git a/intervention/urls.py b/intervention/urls.py index f9249fa9..a2648ace 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -19,7 +19,7 @@ from intervention.views.intervention import new_view, edit_view, remove_view, \ from intervention.views.detail import DetailInterventionView from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView -from intervention.views.report import report_view +from intervention.views.report import InterventionPublicReportView from intervention.views.resubmission import InterventionResubmissionView from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \ get_revocation_view @@ -38,7 +38,7 @@ urlpatterns = [ 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'), + path('/report', InterventionPublicReportView.as_view(), name='report'), path('/resub', InterventionResubmissionView.as_view(), name='resubmission-create'), # Compensations diff --git a/intervention/views/report.py b/intervention/views/report.py index 6bdd8252..01ad7025 100644 --- a/intervention/views/report.py +++ b/intervention/views/report.py @@ -5,72 +5,73 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.http import HttpRequest +from django.http import HttpRequest, HttpResponse 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.decorators import uuid_required from konova.forms import SimpleGeomForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.generators import generate_qr_code +from konova.views.report import AbstractPublicReportView -@uuid_required -def report_view(request: HttpRequest, id: str): - """ Renders the public report view +class InterventionPublicReportView(AbstractPublicReportView): + _TEMPLATE = "intervention/report/report.html" - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders the public report view - Returns: + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention - """ - template = "intervention/report/report.html" - intervention = get_object_or_404(Intervention, id=id) + Returns: + + """ + 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) - 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 = { + "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, + "tables_scrollable": False, 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, - "tables_scrollable": False, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) + return render(request, self._TEMPLATE, context) \ No newline at end of file diff --git a/konova/views/detail.py b/konova/views/detail.py index 3207680f..8b36296f 100644 --- a/konova/views/detail.py +++ b/konova/views/detail.py @@ -4,6 +4,7 @@ Created on: 14.12.25 """ from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest, HttpResponse from django.utils.decorators import method_decorator from django.views import View @@ -21,3 +22,5 @@ class AbstractDetailView(LoginRequiredMixin, View): def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + raise NotImplementedError() diff --git a/konova/views/index.py b/konova/views/index.py index 7c1298fd..65a28656 100644 --- a/konova/views/index.py +++ b/konova/views/index.py @@ -4,6 +4,7 @@ Created on: 14.12.25 """ from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest, HttpResponse from django.utils.decorators import method_decorator from django.views import View @@ -19,3 +20,6 @@ class AbstractIndexView(LoginRequiredMixin, View): @method_decorator(any_group_check) def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) + + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + raise NotImplementedError() diff --git a/konova/views/report.py b/konova/views/report.py new file mode 100644 index 00000000..0f908bd8 --- /dev/null +++ b/konova/views/report.py @@ -0,0 +1,28 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +from abc import abstractmethod + +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest, HttpResponse +from django.utils.decorators import method_decorator +from django.views import View + +from konova.decorators import uuid_required + + +class AbstractPublicReportView(LoginRequiredMixin, View): + _TEMPLATE = None + + class Meta: + abstract = True + + @method_decorator(uuid_required) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) + + @abstractmethod + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + raise NotImplementedError() -- 2.49.1 From a2bda8d230c8a8d7db411a7bbfe497606e9db019 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Sun, 14 Dec 2025 16:43:31 +0100 Subject: [PATCH 05/14] # QR code * refactors qr code generating into class * refactors usage of former qr code method calls --- compensation/views/compensation/report.py | 22 ++++++----- compensation/views/eco_account/report.py | 22 ++++++----- ema/views/report.py | 22 ++++++----- intervention/views/report.py | 23 ++++++----- konova/utils/generators.py | 22 ----------- konova/utils/qrcode.py | 47 +++++++++++++++++++++++ 6 files changed, 100 insertions(+), 58 deletions(-) create mode 100644 konova/utils/qrcode.py diff --git a/compensation/views/compensation/report.py b/compensation/views/compensation/report.py index 077367dd..f7fec85f 100644 --- a/compensation/views/compensation/report.py +++ b/compensation/views/compensation/report.py @@ -14,7 +14,7 @@ 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 +from konova.utils.qrcode import QrCode from konova.views.report import AbstractPublicReportView @@ -48,10 +48,14 @@ class CompensationPublicReportView(AbstractPublicReportView): ) 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) + qrcode = QrCode( + content=request.build_absolute_uri(reverse("compensation:report", args=(id,))), + size=10 + ) + qrcode_lanis = QrCode( + content=comp.get_LANIS_link(), + size=7 + ) # Order states by surface before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type") @@ -61,12 +65,12 @@ class CompensationPublicReportView(AbstractPublicReportView): context = { "obj": comp, "qrcode": { - "img": qrcode_img, - "url": qrcode_url, + "img": qrcode.get_img(), + "url": qrcode.get_content(), }, "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url, + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), }, "is_entry_shared": False, # disables action buttons during rendering "before_states": before_states, diff --git a/compensation/views/eco_account/report.py b/compensation/views/eco_account/report.py index 494c4c8e..beb53cd9 100644 --- a/compensation/views/eco_account/report.py +++ b/compensation/views/eco_account/report.py @@ -14,7 +14,7 @@ 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 +from konova.utils.qrcode import QrCode from konova.views.report import AbstractPublicReportView @@ -48,10 +48,14 @@ class EcoAccountPublicReportView(AbstractPublicReportView): ) 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) + qrcode = QrCode( + content=request.build_absolute_uri(reverse("compensation:acc:report", args=(id,))), + size=10 + ) + qrcode_lanis = QrCode( + content=acc.get_LANIS_link(), + size=7 + ) # Order states by surface before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent") @@ -67,12 +71,12 @@ class EcoAccountPublicReportView(AbstractPublicReportView): context = { "obj": acc, "qrcode": { - "img": qrcode_img, - "url": qrcode_url, + "img": qrcode.get_img(), + "url": qrcode.get_content(), }, "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url, + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), }, "is_entry_shared": False, # disables action buttons during rendering "before_states": before_states, diff --git a/ema/views/report.py b/ema/views/report.py index e9c54f13..058b93b7 100644 --- a/ema/views/report.py +++ b/ema/views/report.py @@ -14,7 +14,7 @@ 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 +from konova.utils.qrcode import QrCode from konova.views.report import AbstractPublicReportView @@ -48,10 +48,14 @@ class EmaPublicReportView(AbstractPublicReportView): ) 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) + qrcode = QrCode( + content=request.build_absolute_uri(reverse("ema:report", args=(id,))), + size=10 + ) + qrcode_lanis = QrCode( + content=ema.get_LANIS_link(), + size=7 + ) # Order states by surface before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type") @@ -61,12 +65,12 @@ class EmaPublicReportView(AbstractPublicReportView): context = { "obj": ema, "qrcode": { - "img": qrcode_img, - "url": qrcode_url + "img": qrcode.get_img(), + "url": qrcode.get_content(), }, "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), }, "is_entry_shared": False, # disables action buttons during rendering "before_states": before_states, diff --git a/intervention/views/report.py b/intervention/views/report.py index 01ad7025..708717a7 100644 --- a/intervention/views/report.py +++ b/intervention/views/report.py @@ -14,7 +14,7 @@ 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 +from konova.utils.qrcode import QrCode from konova.views.report import AbstractPublicReportView @@ -52,21 +52,26 @@ class InterventionPublicReportView(AbstractPublicReportView): 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) + + qrcode = QrCode( + content=request.build_absolute_uri(reverse("intervention:report", args=(id,))), + size=10 + ) + qrcode_lanis = QrCode( + content=intervention.get_LANIS_link(), + size=7 + ) context = { "obj": intervention, "deductions": distinct_deductions, "qrcode": { - "img": qrcode_img, - "url": qrcode_url, + "img": qrcode.get_img(), + "url": qrcode.get_content(), }, "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url, + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), }, "geom_form": geom_form, "parcels": parcels, diff --git a/konova/utils/generators.py b/konova/utils/generators.py index d21b0437..d1980bc2 100644 --- a/konova/utils/generators.py +++ b/konova/utils/generators.py @@ -42,28 +42,6 @@ def generate_random_string(length: int, use_numbers: bool = False, use_letters_l ret_val = "".join(random.choice(elements) for i in range(length)) return ret_val - -def generate_qr_code(content: str, size: int = 20) -> str: - """ Generates a qr code from given content - - Args: - content (str): The content for the qr code - size (int): The image size - - Returns: - qrcode_svg (str): The qr code as svg - """ - qrcode_factory = qrcode.image.svg.SvgImage - qrcode_img = qrcode.make( - content, - image_factory=qrcode_factory, - box_size=size - ) - stream = BytesIO() - qrcode_img.save(stream) - return stream.getvalue().decode() - - class IdentifierGenerator: _MODEL = None diff --git a/konova/utils/qrcode.py b/konova/utils/qrcode.py new file mode 100644 index 00000000..e1498553 --- /dev/null +++ b/konova/utils/qrcode.py @@ -0,0 +1,47 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +from io import BytesIO + +import qrcode +import qrcode.image.svg as svg + + +class QrCode: + """ A wrapping class for creating a qr code with content + + """ + _content = None + _img = None + + def __init__(self, content: str, size: int): + self._content = content + self._img = self._generate_qr_code(content, size) + + def _generate_qr_code(self, content: str, size: int = 20) -> str: + """ Generates a qr code from given content + + Args: + content (str): The content for the qr code + size (int): The image size + + Returns: + qrcode_svg (str): The qr code as svg + """ + img_factory = svg.SvgImage + qrcode_img = qrcode.make( + content, + image_factory=img_factory, + box_size=size + ) + stream = BytesIO() + qrcode_img.save(stream) + return stream.getvalue().decode() + + def get_img(self): + return self._img + + def get_content(self): + return self._content -- 2.49.1 From 1af807deae18f47dbff7f930a013d2fc6946cdf7 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Sun, 14 Dec 2025 17:37:01 +0100 Subject: [PATCH 06/14] # Remove view * refactors remove view methods into classes * introduced AbstractRemoveView * disables final-delete actions from admin views * extends error warnings on RemoveEcoAccountModalForm * removes LoginRequiredMixin from AbstractPublicReportView to make it accessible for the public * updates translations --- compensation/admin.py | 8 ++ compensation/forms/eco_account.py | 10 ++ compensation/urls/compensation.py | 5 +- compensation/urls/eco_account.py | 5 +- .../views/compensation/compensation.py | 32 +------ compensation/views/compensation/remove.py | 20 ++++ compensation/views/eco_account/eco_account.py | 41 +-------- compensation/views/eco_account/remove.py | 22 +++++ ema/urls.py | 5 +- ema/views/ema.py | 24 ----- ema/views/remove.py | 20 ++++ intervention/admin.py | 8 ++ intervention/urls.py | 5 +- intervention/views/intervention.py | 27 +----- intervention/views/remove.py | 19 ++++ konova/views/remove.py | 65 +++++++++++++ konova/views/report.py | 8 +- locale/de/LC_MESSAGES/django.mo | Bin 46187 -> 46258 bytes locale/de/LC_MESSAGES/django.po | 87 +++++++++--------- 19 files changed, 235 insertions(+), 176 deletions(-) create mode 100644 compensation/views/compensation/remove.py create mode 100644 compensation/views/eco_account/remove.py create mode 100644 ema/views/remove.py create mode 100644 intervention/views/remove.py create mode 100644 konova/views/remove.py diff --git a/compensation/admin.py b/compensation/admin.py index fc59451f..67ef2f7a 100644 --- a/compensation/admin.py +++ b/compensation/admin.py @@ -45,6 +45,14 @@ class AbstractCompensationAdmin(BaseObjectAdmin): states = "\n".join(states) return states + def get_actions(self, request): + DELETE_ACTION_IDENTIFIER = "delete_selected" + actions = super().get_actions(request) + + if DELETE_ACTION_IDENTIFIER in actions: + del actions[DELETE_ACTION_IDENTIFIER] + + return actions class CompensationAdmin(AbstractCompensationAdmin): autocomplete_fields = [ diff --git a/compensation/forms/eco_account.py b/compensation/forms/eco_account.py index 42443025..46e6691c 100644 --- a/compensation/forms/eco_account.py +++ b/compensation/forms/eco_account.py @@ -15,6 +15,7 @@ from compensation.models import EcoAccount from intervention.models import Handler, Responsibility, Legal from konova.forms import SimpleGeomForm from konova.forms.modals import RemoveModalForm +from konova.settings import ETS_GROUP from konova.utils import validators from user.models import User, UserActionLogEntry @@ -246,4 +247,13 @@ class RemoveEcoAccountModalForm(RemoveModalForm): "confirm", _("The account can not be removed, since there are still deductions.") ) + + # If there are deductions but the performing user is not part of an ETS group, we assume this poor + # fella does not know what he/she does -> give a hint that they should contact someone in charge... + user_is_ets_user = self.user.in_group(ETS_GROUP) + if not user_is_ets_user: + self.add_error( + "confirm", + _("Please contact the responsible conservation office to find a solution!") + ) return super_valid and not has_deductions diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index c77faed1..b9eaf84f 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -10,6 +10,7 @@ from django.urls import path from compensation.views.compensation.detail import DetailCompensationView from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \ GetCompensationDocumentView, RemoveCompensationDocumentView +from compensation.views.compensation.remove import RemoveCompensationView from compensation.views.compensation.resubmission import CompensationResubmissionView from compensation.views.compensation.report import CompensationPublicReportView from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \ @@ -19,7 +20,7 @@ from compensation.views.compensation.action import NewCompensationActionView, Ed from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \ RemoveCompensationStateView from compensation.views.compensation.compensation import new_view, edit_view, \ - remove_view, IndexCompensationView, CompensationIdentifierGeneratorView + IndexCompensationView, CompensationIdentifierGeneratorView from compensation.views.compensation.log import CompensationLogView urlpatterns = [ @@ -31,7 +32,7 @@ urlpatterns = [ path('', DetailCompensationView.as_view(), name='detail'), path('/log', CompensationLogView.as_view(), name='log'), path('/edit', edit_view, name='edit'), - path('/remove', remove_view, name='remove'), + path('/remove', RemoveCompensationView.as_view(), name='remove'), path('/state/new', NewCompensationStateView.as_view(), name='new-state'), path('/state//edit', EditCompensationStateView.as_view(), name='state-edit'), diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index de99a3f5..a1ade266 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -9,10 +9,11 @@ from django.urls import path from compensation.autocomplete.eco_account import EcoAccountAutocomplete from compensation.views.eco_account.detail import DetailEcoAccountView -from compensation.views.eco_account.eco_account import new_view, edit_view, remove_view, \ +from compensation.views.eco_account.eco_account import new_view, edit_view, \ IndexEcoAccountView, EcoAccountIdentifierGeneratorView from compensation.views.eco_account.log import EcoAccountLogView from compensation.views.eco_account.record import EcoAccountRecordView +from compensation.views.eco_account.remove import RemoveEcoAccountView from compensation.views.eco_account.report import EcoAccountPublicReportView from compensation.views.eco_account.resubmission import EcoAccountResubmissionView from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \ @@ -37,7 +38,7 @@ urlpatterns = [ path('/record', EcoAccountRecordView.as_view(), name='record'), path('/report', EcoAccountPublicReportView.as_view(), name='report'), path('/edit', edit_view, name='edit'), - path('/remove', remove_view, name='remove'), + path('/remove', RemoveEcoAccountView.as_view(), name='remove'), path('/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'), path('/state/new', NewEcoAccountStateView.as_view(), name='new-state'), diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 94fce5f9..3f2990ba 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -10,7 +10,6 @@ from django.contrib.auth.decorators import login_required from django.core.exceptions import ObjectDoesNotExist from django.http import HttpRequest, HttpResponse 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 @@ -18,13 +17,11 @@ 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, login_required_modal +from konova.decorators import shared_access_required, default_group_required from konova.forms import SimpleGeomForm -from konova.forms.modals import RemoveModalForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, \ - RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \ - COMPENSATION_ADDED_TEMPLATE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE +from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, \ + IDENTIFIER_REPLACED, COMPENSATION_ADDED_TEMPLATE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE from konova.views.identifier import AbstractIdentifierGeneratorView from konova.views.index import AbstractIndexView @@ -194,26 +191,3 @@ def edit_view(request: HttpRequest, id: str): } context = BaseContext(request, context).context return render(request, template, context) - -@login_required_modal -@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/remove.py b/compensation/views/compensation/remove.py new file mode 100644 index 00000000..bced8af8 --- /dev/null +++ b/compensation/views/compensation/remove.py @@ -0,0 +1,20 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +from django.http import HttpRequest +from django.utils.decorators import method_decorator + +from compensation.models import Compensation +from konova.decorators import shared_access_required +from konova.views.remove import AbstractRemoveView + + +class RemoveCompensationView(AbstractRemoveView): + _MODEL = Compensation + _REDIRECT_URL = "compensation:index" + + @method_decorator(shared_access_required(Compensation, "id")) + def dispatch(self, request: HttpRequest, id: str, *args, **kwargs): + return super().dispatch(request, id, *args, **kwargs) diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 3acd5714..8c98c5c1 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -9,18 +9,16 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.http import HttpRequest, HttpResponse 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, RemoveEcoAccountModalForm +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, login_required_modal +from konova.decorators import shared_access_required, default_group_required from konova.forms import SimpleGeomForm -from konova.settings import ETS_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 RECORDED_BLOCKS_EDIT, FORM_INVALID, \ IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE from konova.views.identifier import AbstractIdentifierGeneratorView from konova.views.index import AbstractIndexView @@ -171,36 +169,3 @@ def edit_view(request: HttpRequest, id: str): } context = BaseContext(request, context).context return render(request, template, context) - - -@login_required_modal -@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 user.in_group(ETS_GROUP): - messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED) - return redirect("compensation:acc:detail", id=id) - - form = RemoveEcoAccountModalForm(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/remove.py b/compensation/views/eco_account/remove.py new file mode 100644 index 00000000..834a076d --- /dev/null +++ b/compensation/views/eco_account/remove.py @@ -0,0 +1,22 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +from django.http import HttpRequest +from django.utils.decorators import method_decorator + +from compensation.forms.eco_account import RemoveEcoAccountModalForm +from compensation.models import EcoAccount +from konova.decorators import shared_access_required +from konova.views.remove import AbstractRemoveView + + +class RemoveEcoAccountView(AbstractRemoveView): + _MODEL = EcoAccount + _REDIRECT_URL = "compensation:acc:index" + _FORM = RemoveEcoAccountModalForm + + @method_decorator(shared_access_required(EcoAccount, "id")) + def dispatch(self, request: HttpRequest, id: str, *args, **kwargs): + return super().dispatch(request, id, *args, **kwargs) diff --git a/ema/urls.py b/ema/urls.py index 705808bc..d3a928d9 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -11,9 +11,10 @@ from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActio from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView from ema.views.detail import DetailEmaView from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView -from ema.views.ema import new_view, edit_view, remove_view, IndexEmaView, EmaIdentifierGeneratorView +from ema.views.ema import new_view, edit_view, IndexEmaView, EmaIdentifierGeneratorView from ema.views.log import EmaLogView from ema.views.record import EmaRecordView +from ema.views.remove import RemoveEmaView from ema.views.report import EmaPublicReportView from ema.views.resubmission import EmaResubmissionView from ema.views.share import EmaShareFormView, EmaShareByTokenView @@ -27,7 +28,7 @@ urlpatterns = [ path("", DetailEmaView.as_view(), name="detail"), path('/log', EmaLogView.as_view(), name='log'), path('/edit', edit_view, name='edit'), - path('/remove', remove_view, name='remove'), + path('/remove', RemoveEmaView.as_view(), name='remove'), path('/record', EmaRecordView.as_view(), name='record'), path('/report', EmaPublicReportView.as_view(), name='report'), path('/resub', EmaResubmissionView.as_view(), name='resubmission-create'), diff --git a/ema/views/ema.py b/ema/views/ema.py index 9c0a216f..8fed05ea 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -174,27 +174,3 @@ def edit_view(request: HttpRequest, id: str): } context = BaseContext(request, context).context return render(request, template, context) - - -@login_required_modal -@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/remove.py b/ema/views/remove.py new file mode 100644 index 00000000..9e3b3507 --- /dev/null +++ b/ema/views/remove.py @@ -0,0 +1,20 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +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.views.remove import AbstractRemoveView + + +class RemoveEmaView(AbstractRemoveView): + _MODEL = Ema + _REDIRECT_URL = "ema:index" + + @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/admin.py b/intervention/admin.py index 46f7244d..208030e6 100644 --- a/intervention/admin.py +++ b/intervention/admin.py @@ -37,6 +37,14 @@ class InterventionAdmin(BaseObjectAdmin): "geometry", ] + def get_actions(self, request): + DELETE_ACTION_IDENTIFIER = "delete_selected" + actions = super().get_actions(request) + + if DELETE_ACTION_IDENTIFIER in actions: + del actions[DELETE_ACTION_IDENTIFIER] + + return actions class InterventionDocumentAdmin(AbstractDocumentAdmin): pass diff --git a/intervention/urls.py b/intervention/urls.py index a2648ace..6a3855b5 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -14,8 +14,9 @@ from intervention.views.deduction import NewInterventionDeductionView, EditInter RemoveInterventionDeductionView from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \ RemoveInterventionDocumentView, EditInterventionDocumentView -from intervention.views.intervention import new_view, edit_view, remove_view, \ +from intervention.views.intervention import new_view, edit_view, \ IndexInterventionView, InterventionIdentifierGeneratorView +from intervention.views.remove import RemoveInterventionView from intervention.views.detail import DetailInterventionView from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView @@ -33,7 +34,7 @@ urlpatterns = [ path('', DetailInterventionView.as_view(), name='detail'), path('/log', InterventionLogView.as_view(), name='log'), path('/edit', edit_view, name='edit'), - path('/remove', remove_view, name='remove'), + path('/remove', RemoveInterventionView.as_view(), name='remove'), path('/share/', InterventionShareByTokenView.as_view(), name='share-token'), path('/share', InterventionShareFormView.as_view(), name='share-form'), path('/check', check_view, name='check'), diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index 24d25983..7bc3fabb 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -9,16 +9,14 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required from django.http import HttpRequest, HttpResponse 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, login_required_modal +from konova.decorators import default_group_required, shared_access_required from konova.forms import SimpleGeomForm -from konova.forms.modals import RemoveModalForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, \ CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, \ @@ -179,26 +177,3 @@ def edit_view(request: HttpRequest, id: str): context = BaseContext(request, context).context return render(request, template, context) - -@login_required_modal -@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/remove.py b/intervention/views/remove.py new file mode 100644 index 00000000..10b6ae7f --- /dev/null +++ b/intervention/views/remove.py @@ -0,0 +1,19 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +from django.utils.decorators import method_decorator + +from intervention.models import Intervention +from konova.decorators import shared_access_required +from konova.views.remove import AbstractRemoveView + + +class RemoveInterventionView(AbstractRemoveView): + _MODEL = Intervention + _REDIRECT_URL = "intervention:index" + + @method_decorator(shared_access_required(Intervention, "id")) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) diff --git a/konova/views/remove.py b/konova/views/remove.py new file mode 100644 index 00000000..7117d9cd --- /dev/null +++ b/konova/views/remove.py @@ -0,0 +1,65 @@ +""" +Author: Michel Peltriaux +Created on: 14.12.25 + +""" +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest +from django.urls import reverse +from django.utils.decorators import method_decorator +from django.views import View +from django.utils.translation import gettext_lazy as _ + +from konova.decorators import default_group_required +from konova.forms.modals import RemoveModalForm + + +class AbstractRemoveView(LoginRequiredMixin, View): + _MODEL = None + _REDIRECT_URL = None + _FORM = RemoveModalForm + + class Meta: + abstract = True + + @method_decorator(default_group_required) + def dispatch(self, request, *args, **kwargs): + return super().dispatch(request, *args, **kwargs) + + def __process_request(self, request: HttpRequest, id: str): + obj = self._MODEL.objects.get(id=id) + identifier = obj.identifier + form = self._FORM(request.POST or None, instance=obj, request=request) + return form.process_request( + request, + _("{} removed").format(identifier), + redirect_url=reverse(self._REDIRECT_URL) + ) + + def get(self, request: HttpRequest, id: str): + """ GET endpoint for removing via modal form + + Due to the legacy logic of the form (which processes get and post requests directly), we simply need to pipe + the request from GET and POST endpoints directly into the same method. + + Args: + request (HttpRequest): The incoming request + id (str): The uuid id as string + + Returns: + """ + return self.__process_request(request, id) + + def post(self, request: HttpRequest, id: str): + """ POST endpoint for removing via modal form + + Due to the legacy logic of the form (which processes get and post requests directly), we simply need to pipe + the request from GET and POST endpoints directly into the same method. + + Args: + request (HttpRequest): The incoming request + id (str): The uuid id as string + + Returns: + """ + return self.__process_request(request, id) diff --git a/konova/views/report.py b/konova/views/report.py index 0f908bd8..4e1efb28 100644 --- a/konova/views/report.py +++ b/konova/views/report.py @@ -3,9 +3,8 @@ Author: Michel Peltriaux Created on: 14.12.25 """ -from abc import abstractmethod +from abc import abstractmethod, ABC -from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest, HttpResponse from django.utils.decorators import method_decorator from django.views import View @@ -13,12 +12,9 @@ from django.views import View from konova.decorators import uuid_required -class AbstractPublicReportView(LoginRequiredMixin, View): +class AbstractPublicReportView(View, ABC): _TEMPLATE = None - class Meta: - abstract = True - @method_decorator(uuid_required) def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 4f5c1c6cdbcd85104ef8449094f69ddd59f917aa..2cbacbbcbbb40e1071cb010a5a2267ef36267c6e 100644 GIT binary patch delta 11761 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 delta 11673 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\n" "Language-Team: LANGUAGE \n" @@ -448,7 +448,7 @@ 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:114 -#: compensation/views/compensation/compensation.py:120 +#: compensation/views/compensation/compensation.py:121 msgid "New compensation" msgstr "Neue Kompensation" @@ -456,38 +456,38 @@ msgstr "Neue Kompensation" msgid "Edit compensation" msgstr "Bearbeite Kompensation" -#: compensation/forms/eco_account.py:31 compensation/utils/quality.py:97 +#: compensation/forms/eco_account.py:32 compensation/utils/quality.py:97 msgid "Available Surface" msgstr "Verfügbare Fläche" -#: compensation/forms/eco_account.py:34 +#: compensation/forms/eco_account.py:35 msgid "The amount that can be used for deductions" msgstr "Die für Abbuchungen zur Verfügung stehende Menge" -#: compensation/forms/eco_account.py:43 +#: compensation/forms/eco_account.py:44 #: compensation/templates/compensation/detail/eco_account/view.html:67 #: compensation/utils/quality.py:84 msgid "Agreement date" msgstr "Vereinbarungsdatum" -#: compensation/forms/eco_account.py:45 +#: compensation/forms/eco_account.py:46 msgid "When did the parties agree on this?" msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?" -#: compensation/forms/eco_account.py:72 -#: compensation/views/eco_account/eco_account.py:101 +#: compensation/forms/eco_account.py:73 +#: compensation/views/eco_account/eco_account.py:105 msgid "New Eco-Account" msgstr "Neues Ökokonto" -#: compensation/forms/eco_account.py:81 +#: compensation/forms/eco_account.py:82 msgid "Eco-Account XY; Location ABC" msgstr "Ökokonto XY; Flur ABC" -#: compensation/forms/eco_account.py:147 +#: compensation/forms/eco_account.py:148 msgid "Edit Eco-Account" msgstr "Ökokonto bearbeiten" -#: compensation/forms/eco_account.py:183 +#: compensation/forms/eco_account.py:184 msgid "" "{}m² have been deducted from this eco account so far. The given value of {} " "would be too low." @@ -495,12 +495,16 @@ msgstr "" "{}n² wurden bereits von diesem Ökokonto abgebucht. Der eingegebene Wert von " "{} wäre daher zu klein." -#: compensation/forms/eco_account.py:247 +#: compensation/forms/eco_account.py:248 msgid "The account can not be removed, since there are still deductions." msgstr "" "Das Ökokonto kann nicht entfernt werden, da hierzu noch Abbuchungen " "vorliegen." +#: compensation/forms/eco_account.py:257 +msgid "Please contact the responsible conservation office to find a solution!" +msgstr "Kontaktieren Sie die zuständige Naturschutzbehörde um eine Lösung zu finden!" + #: compensation/forms/mixins.py:37 #: compensation/templates/compensation/detail/eco_account/view.html:63 #: compensation/templates/compensation/report/eco_account/report.html:20 @@ -1288,44 +1292,40 @@ msgstr "" msgid "Responsible data" msgstr "Daten zu den verantwortlichen Stellen" -#: compensation/views/compensation/compensation.py:58 +#: compensation/views/compensation/compensation.py:52 msgid "Compensations - Overview" msgstr "Kompensationen - Übersicht" -#: compensation/views/compensation/compensation.py:181 +#: compensation/views/compensation/compensation.py:167 #: konova/utils/message_templates.py:40 msgid "Compensation {} edited" msgstr "Kompensation {} bearbeitet" -#: compensation/views/compensation/compensation.py:196 -#: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:238 -#: intervention/views/intervention.py:253 +#: compensation/views/compensation/compensation.py:190 +#: compensation/views/eco_account/eco_account.py:168 ema/views/ema.py:173 +#: intervention/views/intervention.py:175 msgid "Edit {}" msgstr "Bearbeite {}" #: compensation/views/compensation/report.py:35 -#: compensation/views/eco_account/report.py:36 ema/views/report.py:35 -#: intervention/views/report.py:35 +#: compensation/views/eco_account/report.py:35 ema/views/report.py:35 +#: intervention/views/report.py:36 msgid "Report {}" msgstr "Bericht {}" -#: compensation/views/eco_account/eco_account.py:53 +#: compensation/views/eco_account/eco_account.py:49 msgid "Eco-account - Overview" msgstr "Ökokonten - Übersicht" -#: compensation/views/eco_account/eco_account.py:86 +#: compensation/views/eco_account/eco_account.py:82 msgid "Eco-Account {} added" msgstr "Ökokonto {} hinzugefügt" -#: compensation/views/eco_account/eco_account.py:158 +#: compensation/views/eco_account/eco_account.py:145 msgid "Eco-Account {} edited" msgstr "Ökokonto {} bearbeitet" -#: compensation/views/eco_account/eco_account.py:288 -msgid "Eco-account removed" -msgstr "Ökokonto entfernt" - -#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:108 +#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:107 msgid "New EMA" msgstr "Neue EMA hinzufügen" @@ -1353,22 +1353,18 @@ msgstr "" msgid "Payment funded compensation" msgstr "Ersatzzahlungsmaßnahme" -#: ema/views/ema.py:53 +#: ema/views/ema.py:52 msgid "EMAs - Overview" msgstr "EMAs - Übersicht" -#: ema/views/ema.py:86 +#: ema/views/ema.py:85 msgid "EMA {} added" msgstr "EMA {} hinzugefügt" -#: ema/views/ema.py:223 +#: ema/views/ema.py:150 msgid "EMA {} edited" msgstr "EMA {} bearbeitet" -#: ema/views/ema.py:262 -msgid "EMA removed" -msgstr "EMA entfernt" - #: intervention/forms/intervention.py:49 msgid "Construction XY; Location ABC" msgstr "Bauvorhaben XY; Flur ABC" @@ -1429,7 +1425,7 @@ msgstr "Datum Bestandskraft bzw. Rechtskraft" #: intervention/forms/intervention.py:216 #: intervention/tests/unit/test_forms.py:36 -#: intervention/views/intervention.py:105 +#: intervention/views/intervention.py:109 msgid "New intervention" msgstr "Neuer Eingriff" @@ -1665,22 +1661,18 @@ msgstr "" msgid "Check performed" msgstr "Prüfung durchgeführt" -#: intervention/views/intervention.py:57 +#: intervention/views/intervention.py:53 msgid "Interventions - Overview" msgstr "Eingriffe - Übersicht" -#: intervention/views/intervention.py:90 +#: intervention/views/intervention.py:86 msgid "Intervention {} added" msgstr "Eingriff {} hinzugefügt" -#: intervention/views/intervention.py:236 +#: intervention/views/intervention.py:150 msgid "Intervention {} edited" msgstr "Eingriff {} bearbeitet" -#: intervention/views/intervention.py:278 -msgid "{} removed" -msgstr "{} entfernt" - #: konova/decorators.py:32 msgid "You need to be staff to perform this action!" msgstr "Hierfür müssen Sie Mitarbeiter sein!" @@ -1810,7 +1802,7 @@ msgstr "Nicht editierbar" msgid "Geometry" msgstr "Geometrie" -#: konova/forms/geometry_form.py:100 +#: konova/forms/geometry_form.py:105 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." @@ -2268,8 +2260,9 @@ msgid "" "too small to be valid). These parts have been removed. Please check the " "stored geometry." msgstr "" -"Die Geometrie enthielt {} invalide Bestandteile (z.B. unaussagekräftige Kleinstflächen)." -"Diese Bestandteile wurden automatisch entfernt. Bitte überprüfen Sie die angepasste Geometrie." +"Die Geometrie enthielt {} invalide Bestandteile (z.B. unaussagekräftige " +"Kleinstflächen).Diese Bestandteile wurden automatisch entfernt. Bitte " +"überprüfen Sie die angepasste Geometrie." #: konova/utils/message_templates.py:89 msgid "This intervention has {} revocations" @@ -2330,6 +2323,10 @@ msgstr "{} verzeichnet" msgid "Errors found:" msgstr "Fehler gefunden:" +#: konova/views/remove.py:35 +msgid "{} removed" +msgstr "{} entfernt" + #: konova/views/resubmission.py:39 msgid "Resubmission set" msgstr "Wiedervorlage gesetzt" -- 2.49.1 From 6aad76866fc6e4d10cf6ee8ca6faee74b66221f6 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 15 Dec 2025 09:40:30 +0100 Subject: [PATCH 07/14] # Fixes Permission check order * fixes bug where permissions would be checked on non-logged in users which caused errors --- compensation/views/compensation/remove.py | 6 +++--- compensation/views/eco_account/remove.py | 6 +++--- ema/views/ema.py | 4 ++-- ema/views/remove.py | 5 +++-- intervention/views/remove.py | 5 +++-- konova/views/detail.py | 9 ++++----- konova/views/identifier.py | 10 +++------- konova/views/index.py | 10 +++------- konova/views/remove.py | 21 ++++++++++----------- 9 files changed, 34 insertions(+), 42 deletions(-) diff --git a/compensation/views/compensation/remove.py b/compensation/views/compensation/remove.py index bced8af8..a073d089 100644 --- a/compensation/views/compensation/remove.py +++ b/compensation/views/compensation/remove.py @@ -3,7 +3,7 @@ Author: Michel Peltriaux Created on: 14.12.25 """ -from django.http import HttpRequest +from django.http import HttpRequest, HttpResponse from django.utils.decorators import method_decorator from compensation.models import Compensation @@ -16,5 +16,5 @@ class RemoveCompensationView(AbstractRemoveView): _REDIRECT_URL = "compensation:index" @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request: HttpRequest, id: str, *args, **kwargs): - return super().dispatch(request, id, *args, **kwargs) + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + return super().get(request, *args, **kwargs) diff --git a/compensation/views/eco_account/remove.py b/compensation/views/eco_account/remove.py index 834a076d..b1ab0334 100644 --- a/compensation/views/eco_account/remove.py +++ b/compensation/views/eco_account/remove.py @@ -3,7 +3,7 @@ Author: Michel Peltriaux Created on: 14.12.25 """ -from django.http import HttpRequest +from django.http import HttpRequest, HttpResponse from django.utils.decorators import method_decorator from compensation.forms.eco_account import RemoveEcoAccountModalForm @@ -18,5 +18,5 @@ class RemoveEcoAccountView(AbstractRemoveView): _FORM = RemoveEcoAccountModalForm @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request: HttpRequest, id: str, *args, **kwargs): - return super().dispatch(request, id, *args, **kwargs) + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + return super().get(request, *args, **kwargs) diff --git a/ema/views/ema.py b/ema/views/ema.py index 8fed05ea..0d949727 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -114,8 +114,8 @@ class EmaIdentifierGeneratorView(AbstractIdentifierGeneratorView): _MODEL = Ema @method_decorator(conservation_office_group_required) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + return super().get(request, *args, **kwargs) @login_required @conservation_office_group_required diff --git a/ema/views/remove.py b/ema/views/remove.py index 9e3b3507..07abfae8 100644 --- a/ema/views/remove.py +++ b/ema/views/remove.py @@ -3,6 +3,7 @@ Author: Michel Peltriaux Created on: 14.12.25 """ +from django.http import HttpRequest, HttpResponse from django.utils.decorators import method_decorator from ema.models import Ema @@ -16,5 +17,5 @@ class RemoveEmaView(AbstractRemoveView): @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) + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + return super().get(request, *args, **kwargs) diff --git a/intervention/views/remove.py b/intervention/views/remove.py index 10b6ae7f..defe8730 100644 --- a/intervention/views/remove.py +++ b/intervention/views/remove.py @@ -3,6 +3,7 @@ Author: Michel Peltriaux Created on: 14.12.25 """ +from django.http import HttpRequest, HttpResponse from django.utils.decorators import method_decorator from intervention.models import Intervention @@ -15,5 +16,5 @@ class RemoveInterventionView(AbstractRemoveView): _REDIRECT_URL = "intervention:index" @method_decorator(shared_access_required(Intervention, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + return super().get(request, *args, **kwargs) diff --git a/konova/views/detail.py b/konova/views/detail.py index 8b36296f..49e0156c 100644 --- a/konova/views/detail.py +++ b/konova/views/detail.py @@ -3,6 +3,8 @@ Author: Michel Peltriaux Created on: 14.12.25 """ +from abc import ABC + from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest, HttpResponse from django.utils.decorators import method_decorator @@ -11,16 +13,13 @@ from django.views import View from konova.decorators import uuid_required, any_group_check -class AbstractDetailView(LoginRequiredMixin, View): +class AbstractDetailView(LoginRequiredMixin, View, ABC): _TEMPLATE = None - class Meta: - abstract = True - @method_decorator(uuid_required) - @method_decorator(any_group_check) def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) + @method_decorator(any_group_check) def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: raise NotImplementedError() diff --git a/konova/views/identifier.py b/konova/views/identifier.py index b5fdd4be..ddacbd3f 100644 --- a/konova/views/identifier.py +++ b/konova/views/identifier.py @@ -3,6 +3,8 @@ Author: Michel Peltriaux Created on: 14.12.25 """ +from abc import ABC + from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest, JsonResponse from django.utils.decorators import method_decorator @@ -12,16 +14,10 @@ from konova.decorators import default_group_required from konova.utils.generators import IdentifierGenerator -class AbstractIdentifierGeneratorView(LoginRequiredMixin, View): +class AbstractIdentifierGeneratorView(LoginRequiredMixin, View, ABC): _MODEL = None - class Meta: - abstract = True - @method_decorator(default_group_required) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) - def get(self, request: HttpRequest, *args, **kwargs): generator = IdentifierGenerator(model=self._MODEL) identifier = generator.generate_id() diff --git a/konova/views/index.py b/konova/views/index.py index 65a28656..228c019a 100644 --- a/konova/views/index.py +++ b/konova/views/index.py @@ -3,6 +3,8 @@ Author: Michel Peltriaux Created on: 14.12.25 """ +from abc import ABC + from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest, HttpResponse from django.utils.decorators import method_decorator @@ -11,15 +13,9 @@ from django.views import View from konova.decorators import any_group_check -class AbstractIndexView(LoginRequiredMixin, View): +class AbstractIndexView(LoginRequiredMixin, View, ABC): _TEMPLATE = "generic_index.html" - class Meta: - abstract = True - @method_decorator(any_group_check) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) - def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: raise NotImplementedError() diff --git a/konova/views/remove.py b/konova/views/remove.py index 7117d9cd..24093677 100644 --- a/konova/views/remove.py +++ b/konova/views/remove.py @@ -3,8 +3,10 @@ Author: Michel Peltriaux Created on: 14.12.25 """ +from abc import ABC + from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import HttpRequest +from django.http import HttpRequest, HttpResponse from django.urls import reverse from django.utils.decorators import method_decorator from django.views import View @@ -14,19 +16,16 @@ from konova.decorators import default_group_required from konova.forms.modals import RemoveModalForm -class AbstractRemoveView(LoginRequiredMixin, View): +class AbstractRemoveView(LoginRequiredMixin, View, ABC): _MODEL = None _REDIRECT_URL = None _FORM = RemoveModalForm - class Meta: - abstract = True - - @method_decorator(default_group_required) def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) - def __process_request(self, request: HttpRequest, id: str): + @method_decorator(default_group_required) + def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: obj = self._MODEL.objects.get(id=id) identifier = obj.identifier form = self._FORM(request.POST or None, instance=obj, request=request) @@ -36,7 +35,7 @@ class AbstractRemoveView(LoginRequiredMixin, View): redirect_url=reverse(self._REDIRECT_URL) ) - def get(self, request: HttpRequest, id: str): + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: """ GET endpoint for removing via modal form Due to the legacy logic of the form (which processes get and post requests directly), we simply need to pipe @@ -48,9 +47,9 @@ class AbstractRemoveView(LoginRequiredMixin, View): Returns: """ - return self.__process_request(request, id) + return self.__process_request(request, id, *args, **kwargs) - def post(self, request: HttpRequest, id: str): + def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: """ POST endpoint for removing via modal form Due to the legacy logic of the form (which processes get and post requests directly), we simply need to pipe @@ -62,4 +61,4 @@ class AbstractRemoveView(LoginRequiredMixin, View): Returns: """ - return self.__process_request(request, id) + return self.__process_request(request, id, *args, **kwargs) -- 2.49.1 From 0b84d418dbf7d12e6d5276b51cba98342607016a Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 15 Dec 2025 13:02:11 +0100 Subject: [PATCH 08/14] # (EMA/EIV) Edit and New view * refactors 'new' view methods into classes for eiv and ema * refactors 'edit' view methods into classes for eiv and ema * reorganizes permissions on non-conservation-office users on ema entries * users can now open the log view properly if they have shared access * ema actions that require conservation office permission are now hidden on the frontend for non-conservation-office users --- .../ema/detail/includes/controls.html | 28 +-- ema/tests/test_views.py | 2 +- ema/urls.py | 6 +- ema/views/ema.py | 186 ++++++++++++------ ema/views/log.py | 1 - intervention/urls.py | 8 +- intervention/views/intervention.py | 180 +++++++++++------ 7 files changed, 261 insertions(+), 150 deletions(-) diff --git a/ema/templates/ema/detail/includes/controls.html b/ema/templates/ema/detail/includes/controls.html index 96c7bbb3..672cdb22 100644 --- a/ema/templates/ema/detail/includes/controls.html +++ b/ema/templates/ema/detail/includes/controls.html @@ -15,10 +15,10 @@ - {% if is_ets_member %} + {% if obj.recorded %} {% endif %} + + + {% endif %} {% if is_default_member %} - - + {% endif %} + {% if is_ets_member %} + - - - {% endif %} {% endif %} \ No newline at end of file diff --git a/ema/tests/test_views.py b/ema/tests/test_views.py index 627753ac..e58ff42d 100644 --- a/ema/tests/test_views.py +++ b/ema/tests/test_views.py @@ -118,6 +118,7 @@ class EmaViewTestCase(CompensationViewTestCase): self.index_url, self.detail_url, self.report_url, + self.log_url, ] fail_urls = [ self.new_url, @@ -133,7 +134,6 @@ class EmaViewTestCase(CompensationViewTestCase): self.action_remove_url, self.action_new_url, self.new_doc_url, - self.log_url, self.remove_url, ] self.assert_url_fail(client, fail_urls) diff --git a/ema/urls.py b/ema/urls.py index d3a928d9..a36cbe5c 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -11,7 +11,7 @@ from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActio from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView from ema.views.detail import DetailEmaView from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView -from ema.views.ema import new_view, edit_view, IndexEmaView, EmaIdentifierGeneratorView +from ema.views.ema import IndexEmaView, EmaIdentifierGeneratorView, EditEmaView, NewEmaView from ema.views.log import EmaLogView from ema.views.record import EmaRecordView from ema.views.remove import RemoveEmaView @@ -23,11 +23,11 @@ from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateVie app_name = "ema" urlpatterns = [ path("", IndexEmaView.as_view(), name="index"), - path("new/", new_view, name="new"), + path("new/", NewEmaView.as_view(), name="new"), path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"), path("", DetailEmaView.as_view(), name="detail"), path('/log', EmaLogView.as_view(), name='log'), - path('/edit', edit_view, name='edit'), + path('/edit', EditEmaView.as_view(), name='edit'), path('/remove', RemoveEmaView.as_view(), name='remove'), path('/record', EmaRecordView.as_view(), name='record'), path('/report', EmaPublicReportView.as_view(), name='report'), diff --git a/ema/views/ema.py b/ema/views/ema.py index 0d949727..c9ee4889 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -7,19 +7,19 @@ Created on: 19.08.22 """ from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.http import HttpRequest, JsonResponse, HttpResponse +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ +from django.views.generic.base import View 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, login_required_modal +from konova.decorators import shared_access_required, conservation_office_group_required from konova.forms import SimpleGeomForm -from konova.forms.modals import RemoveModalForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \ GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE @@ -54,23 +54,49 @@ class IndexEmaView(AbstractIndexView): context = BaseContext(request, context).context return render(request, self._TEMPLATE, context) +class NewEmaView(LoginRequiredMixin, View): + _TEMPLATE = "ema/form/view.html" -@login_required -@conservation_office_group_required -def new_view(request: HttpRequest): - """ - Renders a view for a new eco account creation + @method_decorator(conservation_office_group_required) + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """ GET endpoint - Args: - request (HttpRequest): The incoming request + Renders form for new EMA - Returns: + Args: + request (HttpRequest): The incoming request + *args (): + **kwargs (): - """ - 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": + Returns: + + """ + data_form = NewEmaForm(request.POST or None) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New EMA"), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + @method_decorator(conservation_office_group_required) + def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """ POST endpoint + + Processes submitted form + + Args: + request (HttpRequest): The incoming request + *args (): + **kwargs (): + + Returns: + + """ + data_form = NewEmaForm(request.POST or None) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) 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) @@ -94,21 +120,16 @@ def new_view(request: HttpRequest): request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - 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) - + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New EMA"), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) class EmaIdentifierGeneratorView(AbstractIdentifierGeneratorView): _MODEL = Ema @@ -117,33 +138,73 @@ class EmaIdentifierGeneratorView(AbstractIdentifierGeneratorView): def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: return super().get(request, *args, **kwargs) -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing compensations +class EditEmaView(LoginRequiredMixin, View): + _TEMPLATE = "compensation/form/view.html" - Args: - request (HttpRequest): The incoming request + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ GET endpoint - Returns: + Renders form - """ - 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) + Args: + request (HttpRequest): The incoming request + id (str): The ema identifier + *args (): + **kwargs (): - # 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": + Returns: + + """ + # 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(instance=ema) + geom_form = SimpleGeomForm(read_only=False, instance=ema) + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def post(self, request: HttpRequest, id:str, *args, **kwargs) -> HttpResponse: + """ POST endpoint + + Process submitted forms + + Args: + request (HttpRequest): The incoming request + id (str): The id of the ema + *args (): + **kwargs (): + + Returns: + + """ + # 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 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) @@ -153,24 +214,19 @@ def edit_view(request: HttpRequest, id: str): request, GEOMETRY_SIMPLIFIED ) - num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - 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) + messages.error(request, FORM_INVALID, extra_tags="danger", ) + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) diff --git a/ema/views/log.py b/ema/views/log.py index 82162ba4..b468cc15 100644 --- a/ema/views/log.py +++ b/ema/views/log.py @@ -18,7 +18,6 @@ class EmaLogView(AbstractLogView): @method_decorator(login_required_modal) @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 6a3855b5..6676f777 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -14,8 +14,8 @@ from intervention.views.deduction import NewInterventionDeductionView, EditInter RemoveInterventionDeductionView from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \ RemoveInterventionDocumentView, EditInterventionDocumentView -from intervention.views.intervention import new_view, edit_view, \ - IndexInterventionView, InterventionIdentifierGeneratorView +from intervention.views.intervention import IndexInterventionView, InterventionIdentifierGeneratorView, \ + NewInterventionView, EditInterventionView from intervention.views.remove import RemoveInterventionView from intervention.views.detail import DetailInterventionView from intervention.views.log import InterventionLogView @@ -29,11 +29,11 @@ from intervention.views.share import InterventionShareFormView, InterventionShar app_name = "intervention" urlpatterns = [ path("", IndexInterventionView.as_view(), name="index"), - path('new/', new_view, name='new'), + path('new/', NewInterventionView.as_view(), name='new'), path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'), path('', DetailInterventionView.as_view(), name='detail'), path('/log', InterventionLogView.as_view(), name='log'), - path('/edit', edit_view, name='edit'), + path('/edit', EditInterventionView.as_view(), name='edit'), path('/remove', RemoveInterventionView.as_view(), name='remove'), path('/share/', InterventionShareByTokenView.as_view(), name='share-token'), path('/share', InterventionShareFormView.as_view(), name='share-form'), diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index 7bc3fabb..f8808914 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -7,9 +7,12 @@ Created on: 19.08.22 """ from django.contrib import messages from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, render, redirect +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ +from django.views import View from intervention.forms.intervention import EditInterventionForm, NewInterventionForm from intervention.models import Intervention @@ -56,22 +59,46 @@ class IndexInterventionView(AbstractIndexView): return render(request, self._TEMPLATE, context) -@login_required -@default_group_required -def new_view(request: HttpRequest): - """ - Renders a view for a new intervention creation +class NewInterventionView(LoginRequiredMixin, View): + _TEMPLATE = "intervention/form/view.html" - Args: - request (HttpRequest): The incoming request + @method_decorator(default_group_required) + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: - Returns: + """ + Renders a view for a new intervention creation + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + data_form = NewInterventionForm() + geom_form = SimpleGeomForm(read_only=False) + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New intervention"), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + @method_decorator(default_group_required) + def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + + """ + Renders a view for a new intervention creation + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + data_form = NewInterventionForm(request.POST or None) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) - """ - 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) @@ -83,6 +110,7 @@ def new_view(request: HttpRequest): intervention.identifier ) ) + messages.success(request, _("Intervention {} added").format(intervention.identifier)) if geom_form.has_geometry_simplified(): messages.info( @@ -96,52 +124,86 @@ def new_view(request: HttpRequest): request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - 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) + messages.error(request, FORM_INVALID, extra_tags="danger", ) + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New intervention"), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + class InterventionIdentifierGeneratorView(AbstractIdentifierGeneratorView): _MODEL = Intervention -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing interventions +class EditInterventionView(LoginRequiredMixin, View): + _TEMPLATE = "intervention/form/view.html" - Args: - request (HttpRequest): The incoming request + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ + Renders a view for editing interventions - Returns: + Args: + request (HttpRequest): The incoming request + id (str): The intervention identifier - """ - 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) + Returns: + HttpResponse: The rendered view + """ - # 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": + # 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) + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ + Process saved form content + + Args: + request (HttpRequest): The incoming request + id (str): The intervention id + + Returns: + HttpResponse: + """ + # 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 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 @@ -155,25 +217,17 @@ def edit_view(request: HttpRequest, id: str): request, GEOMETRY_SIMPLIFIED ) - num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - 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) - + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) -- 2.49.1 From 02dc0d0a5961ff63368c4adabed7cd10aa7844b9 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Dec 2025 16:21:42 +0100 Subject: [PATCH 09/14] # Check view * refactors method based view into class --- intervention/urls.py | 4 ++-- intervention/views/check.py | 47 ++++++++++++++++++++++--------------- 2 files changed, 30 insertions(+), 21 deletions(-) diff --git a/intervention/urls.py b/intervention/urls.py index 6676f777..2b42ad3e 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -8,7 +8,7 @@ Created on: 30.11.20 from django.urls import path from intervention.autocomplete.intervention import InterventionAutocomplete -from intervention.views.check import check_view +from intervention.views.check import InterventionCheckView from intervention.views.compensation import remove_compensation_view from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \ RemoveInterventionDeductionView @@ -37,7 +37,7 @@ urlpatterns = [ path('/remove', RemoveInterventionView.as_view(), name='remove'), path('/share/', InterventionShareByTokenView.as_view(), name='share-token'), path('/share', InterventionShareFormView.as_view(), name='share-form'), - path('/check', check_view, name='check'), + path('/check', InterventionCheckView.as_view(), name='check'), path('/record', InterventionRecordView.as_view(), name='record'), path('/report', InterventionPublicReportView.as_view(), name='report'), path('/resub', InterventionResubmissionView.as_view(), name='resubmission-create'), diff --git a/intervention/views/check.py b/intervention/views/check.py index 1fae75bb..a217f989 100644 --- a/intervention/views/check.py +++ b/intervention/views/check.py @@ -5,35 +5,44 @@ 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.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404 +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ +from django.views import View 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 +class InterventionCheckView(LoginRequiredMixin, View): -@login_required -@registration_office_group_required -@shared_access_required(Intervention, "id") -def check_view(request: HttpRequest, id: str): - """ Renders check form for an intervention + def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders check form for an intervention - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id + Args: + request (HttpRequest): The incoming request + id (str): Intervention's id - Returns: + 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 - ) + """ + 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 + ) + @method_decorator(registration_office_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, *args, **kwargs) + + @method_decorator(registration_office_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, *args, **kwargs) -- 2.49.1 From e70a8b51d13bcf8141ed6194b5949b82067c5a20 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Dec 2025 16:25:46 +0100 Subject: [PATCH 10/14] # Remove-KOM-from-EIV view * refactors view method into class --- intervention/urls.py | 4 +-- intervention/views/compensation.py | 58 +++++++++++++++++------------- 2 files changed, 35 insertions(+), 27 deletions(-) diff --git a/intervention/urls.py b/intervention/urls.py index 2b42ad3e..cc8ceddb 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -9,7 +9,7 @@ from django.urls import path from intervention.autocomplete.intervention import InterventionAutocomplete from intervention.views.check import InterventionCheckView -from intervention.views.compensation import remove_compensation_view +from intervention.views.compensation import RemoveCompensationFromInterventionView from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \ RemoveInterventionDeductionView from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \ @@ -43,7 +43,7 @@ urlpatterns = [ path('/resub', InterventionResubmissionView.as_view(), name='resubmission-create'), # Compensations - path('/compensation//remove', remove_compensation_view, name='remove-compensation'), + path('/compensation//remove', RemoveCompensationFromInterventionView.as_view(), name='remove-compensation'), # Documents path('/document/new/', NewInterventionDocumentView.as_view(), name='new-doc'), diff --git a/intervention/views/compensation.py b/intervention/views/compensation.py index 704a04e4..b8516d38 100644 --- a/intervention/views/compensation.py +++ b/intervention/views/compensation.py @@ -5,42 +5,50 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.core.exceptions import ObjectDoesNotExist -from django.http import HttpRequest, Http404 +from django.http import HttpRequest, Http404, HttpResponse from django.shortcuts import get_object_or_404 from django.urls import reverse +from django.utils.decorators import method_decorator +from django.views import View from intervention.models import Intervention -from konova.decorators import shared_access_required, login_required_modal +from konova.decorators import shared_access_required 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): - """ Renders a modal view for removing the compensation +class RemoveCompensationFromInterventionView(LoginRequiredMixin, View): - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id + def __process_request(self, request: HttpRequest, id: str, comp_id: str, *args, **kwargs) -> HttpResponse: + """ Renders a modal view for removing the compensation - Returns: + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id - """ - intervention = get_object_or_404(Intervention, id=id) - try: - comp = intervention.compensations.get( - id=comp_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", ) - 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", - ) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request, id: str, comp_id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, comp_id, *args, **kwargs) + + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request, id: str, comp_id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, comp_id, *args, **kwargs) \ No newline at end of file -- 2.49.1 From 3966521cd4ac5040b7d131a7264b5f39f402ad82 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 16 Dec 2025 16:34:44 +0100 Subject: [PATCH 11/14] # Revocation Intervention views * refactors revocation method views for intervention into classes --- intervention/urls.py | 12 +- intervention/views/revocation.py | 189 ++++++++++++++++++------------- 2 files changed, 114 insertions(+), 87 deletions(-) diff --git a/intervention/urls.py b/intervention/urls.py index cc8ceddb..a2fff6fc 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -22,8 +22,8 @@ from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView from intervention.views.report import InterventionPublicReportView 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.revocation import NewInterventionRevocationView, GetInterventionRevocationView, \ + EditInterventionRevocationView, RemoveInterventionRevocationView from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView app_name = "intervention" @@ -57,10 +57,10 @@ urlpatterns = [ path('/deduction//remove', RemoveInterventionDeductionView.as_view(), name='remove-deduction'), # Revocation routes - path('/revocation/new', new_revocation_view, name='new-revocation'), - 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'), + path('/revocation/new', NewInterventionRevocationView.as_view(), name='new-revocation'), + path('/revocation//edit', EditInterventionRevocationView.as_view(), name='edit-revocation'), + path('/revocation//remove', RemoveInterventionRevocationView.as_view(), name='remove-revocation'), + path('revocation/', GetInterventionRevocationView.as_view(), name='get-doc-revocation'), # Autocomplete path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="autocomplete"), diff --git a/intervention/views/revocation.py b/intervention/views/revocation.py index db1dd1d2..0f1db94f 100644 --- a/intervention/views/revocation.py +++ b/intervention/views/revocation.py @@ -6,10 +6,12 @@ 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.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse +from django.utils.decorators import method_decorator +from django.views import View from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \ RemoveRevocationModalForm @@ -19,100 +21,125 @@ 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 +class NewInterventionRevocationView(LoginRequiredMixin, View): + def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders sharing form for an intervention - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id + Args: + request (HttpRequest): The incoming request + id (str): Intervention's id - Returns: + 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( + """ + 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, - DATA_UNSHARED + msg_success=REVOCATION_ADDED, + redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" ) - return redirect("intervention:detail", id=doc.instance.id) - return get_document(doc) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, *args, **kwargs) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, *args, **kwargs) -@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 +class GetInterventionRevocationView(LoginRequiredMixin, View): + @method_decorator(default_group_required) + def get(self, request: HttpRequest, doc_id: str, *args, **kwargs) -> HttpResponse: + """ Returns the revocation document as downloadable file - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id as string - revocation_id (str): The revocation's id as string + Wraps the generic document fetcher function from konova.utils. - Returns: + Args: + request (HttpRequest): The incoming request + doc_id (str): The document id - """ - intervention = get_object_or_404(Intervention, id=id) - revocation = get_object_or_404(Revocation, id=revocation_id) + Returns: - 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" - ) + """ + 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_modal -@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 +class EditInterventionRevocationView(LoginRequiredMixin, View): + def __process_request(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse: + """ 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 + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id as string + revocation_id (str): The revocation's id as string - Returns: + Returns: - """ - intervention = get_object_or_404(Intervention, id=id) - revocation = get_object_or_404(Revocation, id=revocation_id) + """ + 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" - ) + 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" + ) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, revocation_id, *args, **kwargs) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, revocation_id, *args, **kwargs) + + +class RemoveInterventionRevocationView(LoginRequiredMixin, View): + def __process_request(self, request, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse: + """ 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" + ) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, revocation_id, *args, **kwargs) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, revocation_id, *args, **kwargs) \ No newline at end of file -- 2.49.1 From 0e6f8d5b555468f8a89d13bd8a8a57a056c7331b Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 17 Dec 2025 14:24:43 +0100 Subject: [PATCH 12/14] # Compensation New and Edit * refactors compensation new and edit method views into classes --- compensation/urls/compensation.py | 10 +- .../views/compensation/compensation.py | 228 ++++++++++++------ 2 files changed, 155 insertions(+), 83 deletions(-) diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index b9eaf84f..d7f0df8a 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -19,19 +19,19 @@ from compensation.views.compensation.action import NewCompensationActionView, Ed RemoveCompensationActionView from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \ RemoveCompensationStateView -from compensation.views.compensation.compensation import new_view, edit_view, \ - IndexCompensationView, CompensationIdentifierGeneratorView +from compensation.views.compensation.compensation import IndexCompensationView, CompensationIdentifierGeneratorView, \ + EditCompensationView, NewCompensationView from compensation.views.compensation.log import CompensationLogView urlpatterns = [ # Main compensation path("", IndexCompensationView.as_view(), name="index"), path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'), - path('new/', new_view, name='new'), - path('new', new_view, name='new'), + path('new/', NewCompensationView.as_view(), name='new'), + path('new', NewCompensationView.as_view(), name='new'), path('', DetailCompensationView.as_view(), name='detail'), path('/log', CompensationLogView.as_view(), name='log'), - path('/edit', edit_view, name='edit'), + path('/edit', EditCompensationView.as_view(), name='edit'), path('/remove', RemoveCompensationView.as_view(), name='remove'), path('/state/new', NewCompensationStateView.as_view(), name='new-state'), diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 3f2990ba..147ea038 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -6,11 +6,12 @@ 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.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, render, redirect +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ +from django.views import View from compensation.forms.compensation import EditCompensationForm, NewCompensationForm from compensation.models import Compensation @@ -54,36 +55,78 @@ class IndexCompensationView(AbstractIndexView): context = BaseContext(request, context).context return render(request, self._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 +class NewCompensationView(LoginRequiredMixin, View): + _TEMPLATE = "compensation/form/view.html" - Returns: + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "intervention_id")) + def get(self, request: HttpRequest, intervention_id: str = None, *args, **kwargs) -> HttpResponse: + """ + Renders a view for new compensation - """ - 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) + A compensation creation may be called directly from the parent-intervention object. If so - we may take + the intervention's id and directly link the compensation to it. + + Args: + request (HttpRequest): The incoming request + intervention_id (str): The intervention identifier + + Returns: + + """ + if intervention_id: + # If the parent-intervention is recorded, we are not allowed to change anything on it's data. + # Not even adding new child elements like compensations! + intervention = get_object_or_404(Intervention, id=intervention_id) + recording_state_blocks_actions = intervention.is_recorded + if recording_state_blocks_actions: + 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) + + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New compensation"), + } + context = BaseContext(request, context).context + + return render(request, self._TEMPLATE, context) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "intervention_id")) + def post(self, request: HttpRequest, intervention_id: str = None, *args, **kwargs) -> HttpResponse: + + """ + Renders a view for a new compensation creation + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + if intervention_id: + # If the parent-intervention is recorded, we are not allowed to change anything on it's data. + # Not even adding new child elements like compensations! + intervention = get_object_or_404(Intervention, id=intervention_id) + recording_state_blocks_actions = intervention.is_recorded + if recording_state_blocks_actions: + 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) - 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) @@ -101,65 +144,97 @@ def new_view(request: HttpRequest, intervention_id: str = None): request, GEOMETRY_SIMPLIFIED ) - num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - 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) + messages.error(request, FORM_INVALID, extra_tags="danger", ) + + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New compensation"), + } + context = BaseContext(request, context).context + + return render(request, self._TEMPLATE, context) class CompensationIdentifierGeneratorView(AbstractIdentifierGeneratorView): _MODEL = Compensation -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing compensations +class EditCompensationView(LoginRequiredMixin, View): + _TEMPLATE = "compensation/form/view.html" - Args: - request (HttpRequest): The incoming request + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: - Returns: + """ + Renders a view for editing compensations - """ - 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) + Args: + request (HttpRequest): The incoming request - # 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": + Returns: + + """ + # 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) + + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier), + } + context = BaseContext(request, context).context + + return render(request, self._TEMPLATE, context) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + + """ + Renders a view for editing compensations + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + # 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 data_form.is_valid() and geom_form.is_valid(): # Preserve state of intervention checked to determine whether the user must be informed or not # about a change of the check state intervention_is_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_is_checked: @@ -170,24 +245,21 @@ def edit_view(request: HttpRequest, id: str): request, GEOMETRY_SIMPLIFIED ) - num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - 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) + messages.error(request, FORM_INVALID, extra_tags="danger", ) + + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier), + } + context = BaseContext(request, context).context + + return render(request, self._TEMPLATE, context) \ No newline at end of file -- 2.49.1 From 88058d7caf9d47853822529c0c27cb157653e135 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 17 Dec 2025 14:34:04 +0100 Subject: [PATCH 13/14] # EcoAccount New and Edit * refactors new and edit method views into classes --- compensation/urls/eco_account.py | 8 +- compensation/views/eco_account/eco_account.py | 183 ++++++++++++------ 2 files changed, 124 insertions(+), 67 deletions(-) diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index a1ade266..d57f9b24 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -9,8 +9,8 @@ from django.urls import path from compensation.autocomplete.eco_account import EcoAccountAutocomplete from compensation.views.eco_account.detail import DetailEcoAccountView -from compensation.views.eco_account.eco_account import new_view, edit_view, \ - IndexEcoAccountView, EcoAccountIdentifierGeneratorView +from compensation.views.eco_account.eco_account import IndexEcoAccountView, EcoAccountIdentifierGeneratorView, \ + NewEcoAccountView, EditEcoAccountView from compensation.views.eco_account.log import EcoAccountLogView from compensation.views.eco_account.record import EcoAccountRecordView from compensation.views.eco_account.remove import RemoveEcoAccountView @@ -31,13 +31,13 @@ from compensation.views.eco_account.deduction import NewEcoAccountDeductionView, app_name = "acc" urlpatterns = [ path("", IndexEcoAccountView.as_view(), name="index"), - path('new/', new_view, name='new'), + path('new/', NewEcoAccountView.as_view(), name='new'), path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'), path('', DetailEcoAccountView.as_view(), name='detail'), path('/log', EcoAccountLogView.as_view(), name='log'), path('/record', EcoAccountRecordView.as_view(), name='record'), path('/report', EcoAccountPublicReportView.as_view(), name='report'), - path('/edit', edit_view, name='edit'), + path('/edit', EditEcoAccountView.as_view(), name='edit'), path('/remove', RemoveEcoAccountView.as_view(), name='remove'), path('/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'), diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 8c98c5c1..251744e4 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -6,10 +6,12 @@ Created on: 19.08.22 """ from django.contrib import messages -from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect, render +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ +from django.views import View from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm from compensation.models import EcoAccount @@ -52,22 +54,46 @@ class IndexEcoAccountView(AbstractIndexView): return render(request, self._TEMPLATE, context) -@login_required -@default_group_required -def new_view(request: HttpRequest): - """ - Renders a view for a new eco account creation +class NewEcoAccountView(LoginRequiredMixin, View): + _TEMPLATE = "compensation/form/view.html" - Args: - request (HttpRequest): The incoming request + @method_decorator(default_group_required) + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """ + Renders a view for a new eco account creation - Returns: + Args: + request (HttpRequest): The incoming request - """ - 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": + Returns: + + """ + data_form = NewEcoAccountForm(request.POST or None) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) + + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New Eco-Account"), + } + context = BaseContext(request, context).context + + return render(request, self._TEMPLATE, context) + + @method_decorator(default_group_required) + def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + + """ + Renders a view for a new eco account creation + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + data_form = NewEcoAccountForm(request.POST or None) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) 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) @@ -85,58 +111,92 @@ def new_view(request: HttpRequest): request, GEOMETRY_SIMPLIFIED ) - num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - 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) + messages.error(request, FORM_INVALID, extra_tags="danger", ) + + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New Eco-Account"), + } + context = BaseContext(request, context).context + + return render(request, self._TEMPLATE, context) class EcoAccountIdentifierGeneratorView(AbstractIdentifierGeneratorView): _MODEL = EcoAccount -@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 +class EditEcoAccountView(LoginRequiredMixin, View): + _TEMPLATE = "compensation/form/view.html" - Returns: + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: - """ - 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) + """ + Renders a view for editing compensations + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + # 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) + + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + + """ + Renders a view for editing compensations + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + # 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) - # 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": data_form_valid = data_form.is_valid() geom_form_valid = geom_form.is_valid() if data_form_valid and geom_form_valid: @@ -148,24 +208,21 @@ def edit_view(request: HttpRequest, id: str): request, GEOMETRY_SIMPLIFIED ) - num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - 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) + messages.error(request, FORM_INVALID, extra_tags="danger", ) + + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier), + } + context = BaseContext(request, context).context + + return render(request, self._TEMPLATE, context) -- 2.49.1 From 3f33de3626bf2d5c2234d29350493349e3affeae Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 13 Jan 2026 10:35:09 +0100 Subject: [PATCH 14/14] # Analysis, API and Payment views * refactors payment creation, editing and removing into class based views * refactors analysis report methods into class based views * drops unused method view on api app (token generating has been de facto moved into users app long time ago) --- analysis/urls.py | 4 +- analysis/views.py | 129 ++++++++++++++++++++++--------- api/urls/urls.py | 3 - api/views/method_views.py | 35 --------- compensation/urls/payment.py | 6 +- compensation/views/payment.py | 139 ++++++++++++++++++++-------------- 6 files changed, 182 insertions(+), 134 deletions(-) delete mode 100644 api/views/method_views.py diff --git a/analysis/urls.py b/analysis/urls.py index 71eb6665..c51d70f0 100644 --- a/analysis/urls.py +++ b/analysis/urls.py @@ -10,6 +10,6 @@ from analysis.views import * app_name = "analysis" urlpatterns = [ - path("reports/", index_reports_view, name="reports"), - path("reports/", detail_report_view, name="report-detail"), + path("reports/", ReportIndexView.as_view(), name="reports"), + path("reports/", ReportDetailView.as_view(), name="report-detail"), ] \ No newline at end of file diff --git a/analysis/views.py b/analysis/views.py index 6da4a586..525fdd36 100644 --- a/analysis/views.py +++ b/analysis/views.py @@ -1,8 +1,12 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest, HttpResponse from django.shortcuts import render, redirect, get_object_or_404 from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views import View +from django.views.generic import DetailView from analysis.forms import TimespanReportForm from analysis.utils.excel.excel import TempExcelFile @@ -42,57 +46,112 @@ def index_reports_view(request: HttpRequest): context = BaseContext(request, context).context return render(request, template, context) +class ReportIndexView(LoginRequiredMixin, View): + @method_decorator(conservation_office_group_required) + def get(self, request: HttpRequest) -> HttpResponse: -@login_required -@conservation_office_group_required -def detail_report_view(request: HttpRequest, id: str): - """ Renders the detailed report for a conservation office + """ - Args: - request (HttpRequest): The incoming request - id (str): The conservation_office KonovaCode id + Args: + request (HttpRequest): The incoming request - Returns: + Returns: - """ - # Try to resolve the requested office id - cons_office = get_object_or_404( - KonovaCode, - id=id - ) - # Try to resolve the date parameters into Date objects -> redirect if this fails - try: - df = request.GET.get("df", None) - dt = request.GET.get("dt", None) - date_from = timezone.make_aware(timezone.datetime.fromisoformat(df)) - date_to = timezone.make_aware(timezone.datetime.fromisoformat(dt)) - except ValueError: - messages.error( - request, - PARAMS_INVALID, - extra_tags="danger", + """ + template = "analysis/reports/index.html" + form = TimespanReportForm(None) + context = { + "form": form + } + context = BaseContext(request, context).context + return render(request, template, context) + + @method_decorator(conservation_office_group_required) + def post(self, request: HttpRequest) -> HttpResponse: + + """ + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + template = "analysis/reports/index.html" + form = TimespanReportForm(request.POST or None) + if form.is_valid(): + redirect_url = form.save() + return redirect(redirect_url) + else: + messages.error( + request, + FORM_INVALID, + extra_tags="danger", + ) + context = { + "form": form + } + context = BaseContext(request, context).context + return render(request, template, context) + + +class ReportDetailView(LoginRequiredMixin, DetailView): + + @method_decorator(conservation_office_group_required) + def get(self, request: HttpRequest, id: str): + """ Renders the detailed report for a conservation office + + Args: + request (HttpRequest): The incoming request + id (str): The conservation_office KonovaCode id + + Returns: + + """ + # Try to resolve the requested office id + cons_office = get_object_or_404( + KonovaCode, + id=id ) - return redirect("analysis:reports") + # Try to resolve the date parameters into Date objects -> redirect if this fails + try: + df = request.GET.get("df", None) + dt = request.GET.get("dt", None) + date_from = timezone.make_aware(timezone.datetime.fromisoformat(df)) + date_to = timezone.make_aware(timezone.datetime.fromisoformat(dt)) + except ValueError: + messages.error( + request, + PARAMS_INVALID, + extra_tags="danger", + ) + return redirect("analysis:reports") - # Check whether the html default rendering is requested or an alternative - format_param = request.GET.get("format", "html") - report = TimespanReport(id, date_from, date_to) + # Check whether the html default rendering is requested or an alternative + format_param = request.GET.get("format", "html") + report = TimespanReport(id, date_from, date_to) - if format_param == "html": + if format_param == "html": + return self.__handle_html_format(request, report, cons_office) + elif format_param == "excel": + return self.__handle_excel_format(report, cons_office, df, dt) + else: + raise NotImplementedError + + def __handle_html_format(self, request, report: TimespanReport, office: KonovaCode): template = "analysis/reports/detail.html" context = { - "office": cons_office, + "office": office, "report": report, } context = BaseContext(request, context).context return render(request, template, context) - elif format_param == "excel": + + def __handle_excel_format(self, report: TimespanReport, office: KonovaCode, df: str, dt: str): file = TempExcelFile(report.excel_template_path, report.excel_map) response = HttpResponse( content=file.stream, content_type="application/ms-excel", ) - response['Content-Disposition'] = f'attachment; filename={cons_office.long_name}_{df}_{dt}.xlsx' + response['Content-Disposition'] = f'attachment; filename={office.long_name}_{df}_{dt}.xlsx' return response - else: - raise NotImplementedError diff --git a/api/urls/urls.py b/api/urls/urls.py index abe9eacf..fe0ddbbc 100644 --- a/api/urls/urls.py +++ b/api/urls/urls.py @@ -7,11 +7,8 @@ Created on: 21.01.22 """ from django.urls import path, include -from api.views.method_views import generate_new_token_view - app_name = "api" urlpatterns = [ path("v1/", include("api.urls.v1.urls", namespace="v1")), - path("token/generate", generate_new_token_view, name="generate-new-token"), ] \ No newline at end of file diff --git a/api/views/method_views.py b/api/views/method_views.py deleted file mode 100644 index 7c6904a5..00000000 --- a/api/views/method_views.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 27.01.22 - -""" -from django.contrib.auth.decorators import login_required -from django.http import HttpRequest, JsonResponse - -from api.models import APIUserToken - - -@login_required -def generate_new_token_view(request: HttpRequest): - """ Handles request for fetching - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - - if request.method == "GET": - token = APIUserToken() - while APIUserToken.objects.filter(token=token.token).exists(): - token = APIUserToken() - return JsonResponse( - data={ - "gen_data": token.token - } - ) - else: - raise NotImplementedError \ No newline at end of file diff --git a/compensation/urls/payment.py b/compensation/urls/payment.py index b51384dd..d051423b 100644 --- a/compensation/urls/payment.py +++ b/compensation/urls/payment.py @@ -10,7 +10,7 @@ from compensation.views.payment import * app_name = "pay" urlpatterns = [ - path('/new', new_payment_view, name='new'), - path('/remove/', payment_remove_view, name='remove'), - path('/edit/', payment_edit_view, name='edit'), + path('/new', NewPaymentView.as_view(), name='new'), + path('/remove/', RemovePaymentView.as_view(), name='remove'), + path('/edit/', EditPaymentView.as_view(), name='edit'), ] diff --git a/compensation/views/payment.py b/compensation/views/payment.py index 29e89ac3..3059bc06 100644 --- a/compensation/views/payment.py +++ b/compensation/views/payment.py @@ -5,10 +5,12 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 09.08.21 """ +from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse -from django.contrib.auth.decorators import login_required from django.http import HttpRequest from django.shortcuts import get_object_or_404 +from django.utils.decorators import method_decorator +from django.views import View from compensation.forms.modals.payment import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm from compensation.models import Payment @@ -17,72 +19,97 @@ from konova.decorators import default_group_required, shared_access_required from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def new_payment_view(request: HttpRequest, id: str): - """ Renders a modal view for adding new payments +class NewPaymentView(LoginRequiredMixin, View): - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id for which a new payment shall be added + def __process_request(self, request: HttpRequest, id: str): + """ Renders a modal view for adding new payments - Returns: + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id for which a new payment shall be added - """ - intervention = get_object_or_404(Intervention, id=id) - form = NewPaymentForm(request.POST or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=PAYMENT_ADDED, - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" - ) + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + form = NewPaymentForm(request.POST or None, instance=intervention, request=request) + return form.process_request( + request, + msg_success=PAYMENT_ADDED, + redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" + ) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str): + return self.__process_request(request, id=id) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str): + return self.__process_request(request, id=id) -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def payment_remove_view(request: HttpRequest, id: str, payment_id: str): - """ Renders a modal view for removing payments +class RemovePaymentView(LoginRequiredMixin, View): - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - payment_id (str): The payment's id + def __process_request(self, request: HttpRequest, id: str, payment_id: str): + """ Renders a modal view for removing payments - Returns: + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id + payment_id (str): The payment's id - """ - intervention = get_object_or_404(Intervention, id=id) - payment = get_object_or_404(Payment, id=payment_id) - form = RemovePaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request) - return form.process_request( - request=request, - msg_success=PAYMENT_REMOVED, - redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data" - ) + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + payment = get_object_or_404(Payment, id=payment_id) + form = RemovePaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request) + return form.process_request( + request=request, + msg_success=PAYMENT_REMOVED, + redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data" + ) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, payment_id: str): + return self.__process_request(request, id=id, payment_id=payment_id) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str, payment_id: str): + return self.__process_request(request, id=id, payment_id=payment_id) -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def payment_edit_view(request: HttpRequest, id: str, payment_id: str): - """ Renders a modal view for editing payments +class EditPaymentView(LoginRequiredMixin, View): + def __process_request(self, request: HttpRequest, id: str, payment_id: str): + """ Renders a modal view for editing payments - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - payment_id (str): The payment's id + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id + payment_id (str): The payment's id - Returns: + Returns: - """ - intervention = get_object_or_404(Intervention, id=id) - payment = get_object_or_404(Payment, id=payment_id) - form = EditPaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request) - return form.process_request( - request=request, - msg_success=PAYMENT_EDITED, - redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data" - ) + """ + intervention = get_object_or_404(Intervention, id=id) + payment = get_object_or_404(Payment, id=payment_id) + form = EditPaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request) + return form.process_request( + request=request, + msg_success=PAYMENT_EDITED, + redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data" + ) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, payment_id: str): + return self.__process_request(request, id=id, payment_id=payment_id) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str, payment_id: str): + return self.__process_request(request, id=id, payment_id=payment_id) -- 2.49.1