From 1ceffccd4072351d02c6e581e441cb22e2351e00 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 15 Oct 2025 16:00:51 +0200 Subject: [PATCH 01/36] # Index Intervention refactoring * introduces BaseIndexView class * refactors index view for interventions --- intervention/urls.py | 5 ++-- intervention/views/intervention.py | 45 +++++++++------------------- konova/utils/general.py | 19 ++++++++++++ konova/views/base.py | 47 ++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 33 deletions(-) create mode 100644 konova/views/base.py diff --git a/intervention/urls.py b/intervention/urls.py index 8a148197..26051f55 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, \ + InterventionIndexView 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("", InterventionIndexView.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..61d8d2e3 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -7,6 +7,7 @@ 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 JsonResponse, HttpRequest from django.shortcuts import get_object_or_404, render, redirect from django.urls import reverse @@ -25,40 +26,22 @@ 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.base import BaseIndexView -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ - Renders the index view for Interventions +class InterventionIndexView(LoginRequiredMixin, BaseIndexView): + _INDEX_TABLE_CLS = InterventionTable + _TAB_TITLE = _("Interventions - Overview") - 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) + def _get_queryset(self): + qs = Intervention.objects.filter( + deleted=None, + ).select_related( + "legal" + ).order_by( + "-modified__timestamp" + ) + return qs @login_required diff --git a/konova/utils/general.py b/konova/utils/general.py index b7ee0ea8..43341ffd 100644 --- a/konova/utils/general.py +++ b/konova/utils/general.py @@ -5,6 +5,9 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 17.09.21 """ +from django.contrib import messages +from django.utils.translation import gettext_lazy as _ +from django.http import HttpRequest def format_german_float(num) -> str: @@ -19,3 +22,19 @@ def format_german_float(num) -> str: num (str): The number as german Gleitkommazahl """ return format(num, "0,.2f").replace(",", "X").replace(".", ",").replace("X", ".") + + +def check_user_is_in_any_group(request: HttpRequest): + """ + Checks for any group membership. Adds a message in case of having none. + + """ + user = request.user + # Inform user about missing group privileges! + groups = user.groups.all() + if not groups: + messages.info( + request, + _("+++ Attention: You are not part of any group. You won't be able to create, edit or do anything. Please contact an administrator. +++") + ) + return request diff --git a/konova/views/base.py b/konova/views/base.py new file mode 100644 index 00000000..ab862e9b --- /dev/null +++ b/konova/views/base.py @@ -0,0 +1,47 @@ +""" +Author: Michel Peltriaux +Created on: 15.10.25 + +""" +from abc import abstractmethod + +from django.http import HttpRequest +from django.shortcuts import render +from django.views import View + +from konova.contexts import BaseContext +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.utils.general import check_user_is_in_any_group + + +class BaseIndexView(View): + """ Base class for index views + + """ + _TEMPLATE: str = 'generic_index.html' + _TAB_TITLE: str = "CHANGE_ME" + _INDEX_TABLE_CLS = None + + class Meta: + abstract = True + + def dispatch(self, request, *args, **kwargs): + request = check_user_is_in_any_group(request) + return super().dispatch(request, *args, **kwargs) + + def get(self, request: HttpRequest): + qs = self._get_queryset() + table = self._INDEX_TABLE_CLS( + request=request, + queryset=qs + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + @abstractmethod + def _get_queryset(self): + raise NotImplementedError -- 2.47.2 From 21bb988d86620372709bfaa534cb3fb036a78778 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 15 Oct 2025 16:03:53 +0200 Subject: [PATCH 02/36] # Index Compensation refactoring * refactors index view for compensations --- compensation/urls/compensation.py | 6 +-- .../views/compensation/compensation.py | 42 ++++++------------- 2 files changed, 16 insertions(+), 32 deletions(-) diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index 45a11594..8416539e 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, CompensationIndexView from compensation.views.compensation.log import CompensationLogView urlpatterns = [ # Main compensation - path("", index_view, name="index"), + path("", CompensationIndexView.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/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 15bac1f8..944d54fb 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -7,8 +7,8 @@ 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.core.exceptions import ObjectDoesNotExist -from django.db.models import Sum from django.http import HttpRequest, JsonResponse from django.shortcuts import get_object_or_404, render, redirect from django.urls import reverse @@ -28,37 +28,21 @@ 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.base import BaseIndexView -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ - Renders the index view for compensation +class CompensationIndexView(LoginRequiredMixin, BaseIndexView): + _TAB_TITLE = _("Compensations - Overview") + _INDEX_TABLE_CLS = CompensationTable - 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) + def _get_queryset(self): + qs = 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" + ) + return qs @login_required -- 2.47.2 From 67acddf7010b8ced7762fe7fc8e3e26a59442971 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 15 Oct 2025 16:12:21 +0200 Subject: [PATCH 03/36] # Index EcoAccount refactoring * refactors index view for eco account --- compensation/urls/eco_account.py | 6 +-- compensation/views/eco_account/eco_account.py | 40 ++++++------------- 2 files changed, 15 insertions(+), 31 deletions(-) diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index beaae8d9..e4e0ff41 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, EcoAccountIndexView 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("", EcoAccountIndexView.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/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 28dbfc10..e70e5690 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.db.models import Sum +from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse @@ -24,36 +24,20 @@ 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.base import BaseIndexView -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ - Renders the index view for eco accounts +class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView): + _INDEX_TABLE_CLS = EcoAccountTable + _TAB_TITLE = _("Eco-account - Overview") - 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) + def _get_queryset(self): + qs = EcoAccount.objects.filter( + deleted=None, + ).order_by( + "-modified__timestamp" + ) + return qs @login_required -- 2.47.2 From bb71c0fcc8617c4cbb2cd0f555061f3c0b092877 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 15 Oct 2025 16:14:03 +0200 Subject: [PATCH 04/36] # Index Ema refactoring * refactors index view for ema --- ema/urls.py | 4 ++-- ema/views/ema.py | 39 ++++++++++++--------------------------- 2 files changed, 14 insertions(+), 29 deletions(-) diff --git a/ema/urls.py b/ema/urls.py index bff7c41d..3901c704 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, EmaIndexView 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("", EmaIndexView.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..41684cea 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.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.db.models import Sum +from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest, JsonResponse from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse @@ -25,35 +25,20 @@ 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.base import BaseIndexView -@login_required -def index_view(request: HttpRequest): - """ Renders the index view for EMAs +class EmaIndexView(LoginRequiredMixin, BaseIndexView): + _TAB_TITLE = _("EMAs - Overview") + _INDEX_TABLE_CLS = EmaTable - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "generic_index.html" - 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) + def _get_queryset(self): + qs = Ema.objects.filter( + deleted=None, + ).order_by( + "-modified__timestamp" + ) + return qs @login_required -- 2.47.2 From a44d8658d40583b1ed565cb6a3b0d59e27409892 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 15 Oct 2025 16:29:05 +0200 Subject: [PATCH 05/36] # NewId Generator Ema refactoring * introduces BaseNewIdentifierGeneratorView class * refactors new identifier generator view for ema --- ema/urls.py | 5 +++-- ema/views/ema.py | 28 ++++++++++------------------ konova/views/base.py | 24 +++++++++++++++++++++++- 3 files changed, 36 insertions(+), 21 deletions(-) diff --git a/ema/urls.py b/ema/urls.py index 3901c704..5fad0959 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -10,7 +10,8 @@ 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 new_view, new_id_view, detail_view, edit_view, remove_view, EmaIndexView +from ema.views.ema import new_view, detail_view, edit_view, remove_view, EmaIndexView, \ + EmaIdentifierGeneratorView from ema.views.log import EmaLogView from ema.views.record import EmaRecordView from ema.views.report import report_view @@ -22,7 +23,7 @@ app_name = "ema" urlpatterns = [ path("", EmaIndexView.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("", detail_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 41684cea..4cd9d7ea 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -24,8 +24,8 @@ 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 -from konova.views.base import BaseIndexView + DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, MISSING_GROUP_PERMISSION +from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView class EmaIndexView(LoginRequiredMixin, BaseIndexView): @@ -96,23 +96,15 @@ def new_view(request: HttpRequest): return render(request, template, context) -@login_required -@conservation_office_group_required -def new_id_view(request: HttpRequest): - """ JSON endpoint +class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): + _MODEL_CLS = Ema - 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 - } - ) + def dispatch(self, request, *args, **kwargs): + is_ets = request.user.is_ets_user() + if not is_ets: + messages.info(request, MISSING_GROUP_PERMISSION) + return redirect(reverse("ema:index")) + return super().dispatch(request, *args, **kwargs) @login_required diff --git a/konova/views/base.py b/konova/views/base.py index ab862e9b..6a3c78d6 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -5,7 +5,7 @@ Created on: 15.10.25 """ from abc import abstractmethod -from django.http import HttpRequest +from django.http import HttpRequest, JsonResponse from django.shortcuts import render from django.views import View @@ -45,3 +45,25 @@ class BaseIndexView(View): @abstractmethod def _get_queryset(self): raise NotImplementedError + + +class BaseIdentifierGeneratorView(View): + _MODEL_CLS = None + + class Meta: + abstract = True + + def dispatch(self, request, *args, **kwargs): + request = check_user_is_in_any_group(request) + return super().dispatch(request, *args, **kwargs) + + def get(self, request: HttpRequest): + tmp_obj = self._MODEL_CLS() + identifier = tmp_obj.generate_new_identifier() + while self._MODEL_CLS.objects.filter(identifier=identifier).exists(): + identifier = tmp_obj.generate_new_identifier() + return JsonResponse( + data={ + "gen_data": identifier + } + ) \ No newline at end of file -- 2.47.2 From c597e1934be0082e337fc2bca8dabcd7ca2f90f3 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 15 Oct 2025 16:40:35 +0200 Subject: [PATCH 06/36] # Identifier Generator View refactoring * refactors identifier generator view for interventions * simplifies same view for ema --- ema/views/ema.py | 9 +++------ intervention/urls.py | 6 +++--- intervention/views/intervention.py | 23 ++++++----------------- konova/views/base.py | 23 ++++++++++++++++++++--- 4 files changed, 32 insertions(+), 29 deletions(-) diff --git a/ema/views/ema.py b/ema/views/ema.py index 4cd9d7ea..1b449b6a 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -98,13 +98,10 @@ def new_view(request: HttpRequest): class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): _MODEL_CLS = Ema + _REDIRECT_URL_NAME = "ema:index" - def dispatch(self, request, *args, **kwargs): - is_ets = request.user.is_ets_user() - if not is_ets: - messages.info(request, MISSING_GROUP_PERMISSION) - return redirect(reverse("ema:index")) - return super().dispatch(request, *args, **kwargs) + def _user_has_permission(self, user): + return user.is_ets_user() @login_required diff --git a/intervention/urls.py b/intervention/urls.py index 26051f55..b88fdfa8 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, \ - InterventionIndexView +from intervention.views.intervention import new_view, detail_view, edit_view, remove_view, \ + InterventionIndexView, InterventionIdentifierGeneratorView from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView from intervention.views.report import report_view @@ -28,7 +28,7 @@ app_name = "intervention" urlpatterns = [ path("", InterventionIndexView.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('', detail_view, name='detail'), path('/log', InterventionLogView.as_view(), name='log'), path('/edit', edit_view, name='edit'), diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index 61d8d2e3..a53a5650 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -26,7 +26,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.base import BaseIndexView +from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView class InterventionIndexView(LoginRequiredMixin, BaseIndexView): @@ -100,23 +100,12 @@ def new_view(request: HttpRequest): return render(request, template, context) -@login_required -@default_group_required -def new_id_view(request: HttpRequest): - """ JSON endpoint +class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): + _MODEL_CLS = Intervention + _REDIRECT_URL_NAME = "intervention:index" - 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 - } - ) + def _user_has_permission(self, user): + return user.is_default_user() @login_required diff --git a/konova/views/base.py b/konova/views/base.py index 6a3c78d6..b777dcca 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -5,13 +5,16 @@ Created on: 15.10.25 """ from abc import abstractmethod +from django.contrib import messages from django.http import HttpRequest, JsonResponse -from django.shortcuts import render +from django.shortcuts import render, redirect +from django.urls import reverse from django.views import View from konova.contexts import BaseContext from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.general import check_user_is_in_any_group +from konova.utils.message_templates import MISSING_GROUP_PERMISSION class BaseIndexView(View): @@ -49,12 +52,15 @@ class BaseIndexView(View): class BaseIdentifierGeneratorView(View): _MODEL_CLS = None + _REDIRECT_URL_NAME: str = "home" class Meta: abstract = True def dispatch(self, request, *args, **kwargs): - request = check_user_is_in_any_group(request) + if not self._user_has_permission(request.user): + messages.info(request, MISSING_GROUP_PERMISSION) + return redirect(reverse(self._REDIRECT_URL_NAME)) return super().dispatch(request, *args, **kwargs) def get(self, request: HttpRequest): @@ -66,4 +72,15 @@ class BaseIdentifierGeneratorView(View): data={ "gen_data": identifier } - ) \ No newline at end of file + ) + + def _user_has_permission(self, user): + """ Has to be implemented in inheriting classes! + + Args: + user (): + + Returns: + + """ + raise NotImplementedError -- 2.47.2 From 80e8925a633dd9538d3a6c0ed5fe28af5399628a Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 15 Oct 2025 16:42:42 +0200 Subject: [PATCH 07/36] # Identifier Generator View Compensation refactoring * refactors identifier generator view for compensation --- compensation/urls/compensation.py | 6 ++--- .../views/compensation/compensation.py | 23 +++++-------------- 2 files changed, 9 insertions(+), 20 deletions(-) diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index 8416539e..d5aca228 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -17,14 +17,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, detail_view, edit_view, \ - remove_view, CompensationIndexView +from compensation.views.compensation.compensation import new_view, detail_view, edit_view, \ + remove_view, CompensationIndexView, CompensationIdentifierGeneratorView from compensation.views.compensation.log import CompensationLogView urlpatterns = [ # Main compensation path("", CompensationIndexView.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('', detail_view, name='detail'), diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 944d54fb..4259bf5b 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -28,7 +28,7 @@ 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.base import BaseIndexView +from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView class CompensationIndexView(LoginRequiredMixin, BaseIndexView): @@ -115,23 +115,12 @@ 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 +class CompensationIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): + _MODEL_CLS = Compensation + _REDIRECT_URL_NAME = "compensation:index" - 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 - } - ) + def _user_has_permission(self, user): + return user.is_default_user() @login_required -- 2.47.2 From be9f6f1b7e924579e1b1402be418ac2114f1dfce Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 15 Oct 2025 16:46:07 +0200 Subject: [PATCH 08/36] # Identifier Generator View EcoAccount refactoring * refactors identifier generator view for ecoaccount * simplifies base identifier generator view even further --- compensation/urls/eco_account.py | 6 ++--- .../views/compensation/compensation.py | 3 --- compensation/views/eco_account/eco_account.py | 22 ++++--------------- intervention/views/intervention.py | 3 --- konova/views/base.py | 4 ++-- 5 files changed, 9 insertions(+), 29 deletions(-) diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index e4e0ff41..c0edc4b3 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 new_view, new_id_view, edit_view, remove_view, \ - detail_view, EcoAccountIndexView +from compensation.views.eco_account.eco_account import new_view, edit_view, remove_view, \ + detail_view, EcoAccountIndexView, 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 @@ -30,7 +30,7 @@ app_name = "acc" urlpatterns = [ path("", EcoAccountIndexView.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('', detail_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 4259bf5b..4e6525a8 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -119,9 +119,6 @@ class CompensationIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGene _MODEL_CLS = Compensation _REDIRECT_URL_NAME = "compensation:index" - def _user_has_permission(self, user): - return user.is_default_user() - @login_required @default_group_required diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index e70e5690..fd0a7152 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -24,7 +24,7 @@ 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.base import BaseIndexView +from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView): @@ -96,23 +96,9 @@ def new_view(request: HttpRequest): 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(LoginRequiredMixin, BaseIdentifierGeneratorView): + _MODEL_CLS = EcoAccount + _REDIRECT_URL_NAME = "compensation:acc:index" @login_required diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index a53a5650..6ac85aee 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -104,9 +104,6 @@ class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGene _MODEL_CLS = Intervention _REDIRECT_URL_NAME = "intervention:index" - def _user_has_permission(self, user): - return user.is_default_user() - @login_required @any_group_check diff --git a/konova/views/base.py b/konova/views/base.py index b777dcca..abffa13d 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -75,7 +75,7 @@ class BaseIdentifierGeneratorView(View): ) def _user_has_permission(self, user): - """ Has to be implemented in inheriting classes! + """ Should be overwritten in inheriting classes! Args: user (): @@ -83,4 +83,4 @@ class BaseIdentifierGeneratorView(View): Returns: """ - raise NotImplementedError + return user.is_default_user() -- 2.47.2 From afbdf221c3b0f31ec4ffbf509b2698ffd408983e Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 15 Oct 2025 17:09:40 +0200 Subject: [PATCH 09/36] # User view refactoring * refactors majority of user views into class based views * introduces BaseModalFormView and BaseView for even more generic usage * renames url identifier user:index into user:detail for more clarity --- konova/views/base.py | 18 +++- templates/navbars/navbar.html | 2 +- user/forms/user.py | 2 +- user/tests/test_views.py | 2 +- user/tests/unit/test_forms.py | 2 +- user/urls.py | 10 +- user/views/views.py | 177 +++++++++++++++------------------- 7 files changed, 103 insertions(+), 110 deletions(-) diff --git a/konova/views/base.py b/konova/views/base.py index abffa13d..e1548a95 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -17,12 +17,24 @@ from konova.utils.general import check_user_is_in_any_group from konova.utils.message_templates import MISSING_GROUP_PERMISSION -class BaseIndexView(View): +class BaseView(View): + _TEMPLATE: str = "CHANGE_ME" + _TAB_TITLE: str = "CHANGE_ME" + + class Meta: + abstract = True + + +class BaseModalFormView(BaseView): + _TEMPLATE = "modal/modal_form.html" + _TAB_TITLE = None + + +class BaseIndexView(BaseView): """ Base class for index views """ - _TEMPLATE: str = 'generic_index.html' - _TAB_TITLE: str = "CHANGE_ME" + _TEMPLATE = "generic_index.html" _INDEX_TABLE_CLS = None class Meta: diff --git a/templates/navbars/navbar.html b/templates/navbars/navbar.html index 9d1d6859..7ba2132d 100644 --- a/templates/navbars/navbar.html +++ b/templates/navbars/navbar.html @@ -56,7 +56,7 @@ {% if user.is_staff or user.is_superuser %} {% fa5_icon 'tools' %} {% trans 'Admin' %} {% endif %} - {% fa5_icon 'cogs' %} {% trans 'Settings' %} + {% fa5_icon 'cogs' %} {% trans 'Settings' %} {% fa5_icon 'sign-out-alt' %} {% trans 'Logout' %} diff --git a/user/forms/user.py b/user/forms/user.py index 190ce126..0f4c4f24 100644 --- a/user/forms/user.py +++ b/user/forms/user.py @@ -38,7 +38,7 @@ class UserNotificationForm(BaseForm): self.form_title = _("Edit notifications") self.form_caption = _("") self.action_url = reverse("user:notifications") - self.cancel_redirect = reverse("user:index") + self.cancel_redirect = reverse("user:detail") # Insert all notifications into form field by creating choices as tuples notifications = UserNotification.objects.filter( diff --git a/user/tests/test_views.py b/user/tests/test_views.py index fe4c854f..7b2d2406 100644 --- a/user/tests/test_views.py +++ b/user/tests/test_views.py @@ -26,7 +26,7 @@ class UserViewTestCase(BaseViewTestCase): self.team.users.add(self.superuser) self.team.admins.add(self.superuser) # Prepare urls - self.index_url = reverse("user:index", args=()) + self.index_url = reverse("user:detail", args=()) self.notification_url = reverse("user:notifications", args=()) self.api_token_url = reverse("user:api-token", args=()) self.contact_url = reverse("user:contact", args=(self.superuser.id,)) diff --git a/user/tests/unit/test_forms.py b/user/tests/unit/test_forms.py index 20e4333c..4a86945f 100644 --- a/user/tests/unit/test_forms.py +++ b/user/tests/unit/test_forms.py @@ -233,7 +233,7 @@ class UserNotificationFormTestCase(BaseTestCase): self.assertEqual(form.form_title, str(_("Edit notifications"))) self.assertEqual(form.form_caption, "") self.assertEqual(form.action_url, reverse("user:notifications")) - self.assertEqual(form.cancel_redirect, reverse("user:index")) + self.assertEqual(form.cancel_redirect, reverse("user:detail")) def test_save(self): selected_notification = UserNotification.objects.first() diff --git a/user/urls.py b/user/urls.py index c3127a1e..ce8616fd 100644 --- a/user/urls.py +++ b/user/urls.py @@ -15,15 +15,15 @@ from user.views.views import * app_name = "user" urlpatterns = [ - path("", index_view, name="index"), + path("", UserDetailView.as_view(), name="detail"), path("propagate/", PropagateUserView.as_view(), name="propagate"), - path("notifications/", notifications_view, name="notifications"), + path("notifications/", NotificationsView.as_view(), name="notifications"), path("token/api", APITokenView.as_view(), name="api-token"), path("token/api/new", new_api_token_view, name="api-token-new"), - path("contact/", contact_view, name="contact"), - path("team/", index_team_view, name="team-index"), + path("contact/", ContactView.as_view(), name="contact"), + path("team/", TeamIndexView.as_view(), name="team-index"), path("team/new", new_team_view, name="team-new"), - path("team/", data_team_view, name="team-data"), + path("team/", TeamDetailModalView.as_view(), name="team-data"), path("team//edit", edit_team_view, name="team-edit"), path("team//remove", remove_team_view, name="team-remove"), path("team//leave", leave_team_view, name="team-leave"), diff --git a/user/views/views.py b/user/views/views.py index 5d773926..e639703c 100644 --- a/user/views/views.py +++ b/user/views/views.py @@ -1,8 +1,10 @@ from django.contrib import messages from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.views.base import BaseView, BaseModalFormView from user.forms.modals.team import NewTeamModalForm, EditTeamModalForm, RemoveTeamModalForm, LeaveTeamModalForm from user.forms.modals.user import UserContactForm from user.forms.team import TeamDataForm @@ -13,129 +15,108 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.utils.translation import gettext_lazy as _ from konova.contexts import BaseContext -from konova.decorators import any_group_check, login_required_modal +from konova.decorators import login_required_modal -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ Renders the user's data index view +class UserDetailView(LoginRequiredMixin, BaseView): + _TAB_TITLE = _("User settings") + _TEMPLATE = "user/index.html" - Args: - request (): - - Returns: - - """ - template = "user/index.html" - context = { - "user": request.user, - TAB_TITLE_IDENTIFIER: _("User settings"), - } - context = BaseContext(request, context).context - return render(request, template, context) + def get(self, request: HttpRequest): + context = { + "user": request.user, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) -@login_required -@any_group_check -def notifications_view(request: HttpRequest): - """ Renders the notifications settings view +class NotificationsView(LoginRequiredMixin, BaseView): + _TEMPLATE = "user/notifications.html" + _TAB_TITLE = _("User notifications") - Args: - request (): + def get(self, request: HttpRequest): + user = request.user + form = UserNotificationForm(user=user, data=None) + context = { + "user": user, + "form": form, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) - Returns: - - """ - template = "user/notifications.html" - user = request.user - - form = UserNotificationForm(user=user, data=request.POST or None) - if request.method == "POST": + def post(self, request: HttpRequest): + user = request.user + form = UserNotificationForm(user=user, data=request.POST) if form.is_valid(): form.save() messages.success( request, _("Notifications edited") ) - return redirect("user:index") - elif request.method == "GET": - # Implicit - pass - else: - raise NotImplementedError - - context = { - "user": user, - "form": form, - TAB_TITLE_IDENTIFIER: _("User notifications"), - } - context = BaseContext(request, context).context - return render(request, template, context) + return redirect("user:detail") + context = { + "user": user, + "form": form, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) -@login_required_modal -@login_required -def contact_view(request: HttpRequest, id: str): - """ Renders contact modal view of a users contact data +class ContactView(LoginRequiredMixin, BaseModalFormView): + def get(self, request: HttpRequest, id: str): + """ Renders contact modal view of a users contact data - Args: - request (HttpRequest): The incoming request - id (str): The user's id + Args: + request (HttpRequest): The incoming request + id (str): The user's id - Returns: + Returns: - """ - user = get_object_or_404(User, id=id) - form = UserContactForm(request.POST or None, instance=user, request=request) - template = "modal/modal_form.html" - context = { - "form": form, - } - context = BaseContext(request, context).context - return render( - request, - template, - context - ) + """ + user = get_object_or_404(User, id=id) + form = UserContactForm(request.POST or None, instance=user, request=request) + context = { + "form": form, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) -@login_required_modal -@login_required -def data_team_view(request: HttpRequest, id: str): - """ Renders team data +class TeamDetailModalView(LoginRequiredMixin, BaseModalFormView): + def get(self, request: HttpRequest, id: str): + """ Renders team data - Args: - request (HttpRequest): The incoming request - id (str): The team's id + Args: + request (HttpRequest): The incoming request + id (str): The team's id - Returns: + Returns: - """ - team = get_object_or_404(Team, id=id) - form = TeamDataForm(request.POST or None, instance=team, request=request) - template = "modal/modal_form.html" - context = { - "form": form, - } - context = BaseContext(request, context).context - return render( - request, - template, - context - ) + """ + team = get_object_or_404(Team, id=id) + form = TeamDataForm(request.POST or None, instance=team, request=request) + context = { + "form": form, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) -@login_required -def index_team_view(request: HttpRequest): - template = "user/team/index.html" - user = request.user - context = { - "teams": user.shared_teams, - "tab_title": _("Teams"), - } - context = BaseContext(request, context).context - return render(request, template, context) +class TeamIndexView(LoginRequiredMixin, BaseView): + _TEMPLATE = "user/team/index.html" + _TAB_TITLE = _("Teams") + + def get(self, request: HttpRequest): + user = request.user + context = { + "teams": user.shared_teams, + "tab_title": self._TAB_TITLE, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) @login_required_modal -- 2.47.2 From 242730435e10796988030200a88368035153e68d Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 16 Oct 2025 15:36:57 +0200 Subject: [PATCH 10/36] # Deduction views * refactors deduction views on interventions and eco accounts from function to class based * introduces basic checks on shared access and permission on BaseView on dispatching --> checks shall be overwritten on inheriting classes --- codelist/views.py | 0 .../views/compensation/compensation.py | 2 +- compensation/views/eco_account/deduction.py | 48 ++++---------- compensation/views/eco_account/eco_account.py | 2 +- ema/views/ema.py | 2 +- intervention/views/deduction.py | 49 +++----------- intervention/views/intervention.py | 2 +- konova/views/base.py | 61 +++++++++++++++--- konova/views/deduction.py | 64 +++++++++++++------ user/views/views.py | 32 ++++++++-- 10 files changed, 148 insertions(+), 114 deletions(-) delete mode 100644 codelist/views.py diff --git a/codelist/views.py b/codelist/views.py deleted file mode 100644 index e69de29b..00000000 diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 4e6525a8..749a88aa 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -117,7 +117,7 @@ def new_view(request: HttpRequest, intervention_id: str = None): class CompensationIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): _MODEL_CLS = Compensation - _REDIRECT_URL_NAME = "compensation:index" + _REDIRECT_URL = "compensation:index" @login_required diff --git a/compensation/views/eco_account/deduction.py b/compensation/views/eco_account/deduction.py index 1de6c605..80040a20 100644 --- a/compensation/views/eco_account/deduction.py +++ b/compensation/views/eco_account/deduction.py @@ -5,54 +5,28 @@ 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.http import Http404 -from django.utils.decorators import method_decorator from compensation.models import EcoAccount -from konova.decorators import default_group_required, login_required_modal from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView -class NewEcoAccountDeductionView(AbstractNewDeductionView): - model = EcoAccount - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) +class NewEcoAccountDeductionView(LoginRequiredMixin, AbstractNewDeductionView): + _MODEL = EcoAccount + _REDIRECT_URL = "compensation:acc:detail" def _custom_check(self, obj): + # New deductions can only be created if the eco account has been recorded if not obj.recorded: raise Http404() -class EditEcoAccountDeductionView(AbstractEditDeductionView): - def _custom_check(self, obj): - pass - - model = EcoAccount - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) +class EditEcoAccountDeductionView(LoginRequiredMixin, AbstractEditDeductionView): + _MODEL = EcoAccount + _REDIRECT_URL = "compensation:acc:detail" -class RemoveEcoAccountDeductionView(AbstractRemoveDeductionView): - def _custom_check(self, obj): - pass - - model = EcoAccount - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) - +class RemoveEcoAccountDeductionView(LoginRequiredMixin, AbstractRemoveDeductionView): + _MODEL = EcoAccount + _REDIRECT_URL = "compensation:acc:detail" diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index fd0a7152..55a56a2e 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -98,7 +98,7 @@ def new_view(request: HttpRequest): class EcoAccountIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): _MODEL_CLS = EcoAccount - _REDIRECT_URL_NAME = "compensation:acc:index" + _REDIRECT_URL = "compensation:acc:index" @login_required diff --git a/ema/views/ema.py b/ema/views/ema.py index 1b449b6a..8ec9a8b1 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -98,7 +98,7 @@ def new_view(request: HttpRequest): class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): _MODEL_CLS = Ema - _REDIRECT_URL_NAME = "ema:index" + _REDIRECT_URL = "ema:index" def _user_has_permission(self, user): return user.is_ets_user() diff --git a/intervention/views/deduction.py b/intervention/views/deduction.py index 962fe807..122c1dba 100644 --- a/intervention/views/deduction.py +++ b/intervention/views/deduction.py @@ -5,51 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator +from django.contrib.auth.mixins import LoginRequiredMixin from intervention.models import Intervention -from konova.decorators import default_group_required, shared_access_required from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView -class NewInterventionDeductionView(AbstractNewDeductionView): - def _custom_check(self, obj): - pass - - model = Intervention - redirect_url = "intervention:detail" - - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Intervention, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) +class NewInterventionDeductionView(LoginRequiredMixin, AbstractNewDeductionView): + _MODEL = Intervention + _REDIRECT_URL = "intervention:detail" -class EditInterventionDeductionView(AbstractEditDeductionView): - def _custom_check(self, obj): - pass - - model = Intervention - redirect_url = "intervention:detail" - - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Intervention, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) +class EditInterventionDeductionView(LoginRequiredMixin, AbstractEditDeductionView): + _MODEL = Intervention + _REDIRECT_URL = "intervention:detail" -class RemoveInterventionDeductionView(AbstractRemoveDeductionView): - def _custom_check(self, obj): - pass - - model = Intervention - redirect_url = "intervention:detail" - - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Intervention, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) +class RemoveInterventionDeductionView(LoginRequiredMixin, AbstractRemoveDeductionView): + _MODEL = Intervention + _REDIRECT_URL = "intervention:detail" diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index 6ac85aee..80a92b51 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -102,7 +102,7 @@ def new_view(request: HttpRequest): class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): _MODEL_CLS = Intervention - _REDIRECT_URL_NAME = "intervention:index" + _REDIRECT_URL = "intervention:index" @login_required diff --git a/konova/views/base.py b/konova/views/base.py index e1548a95..9a6099c1 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -14,21 +14,57 @@ from django.views import View from konova.contexts import BaseContext from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.general import check_user_is_in_any_group -from konova.utils.message_templates import MISSING_GROUP_PERMISSION +from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED class BaseView(View): _TEMPLATE: str = "CHANGE_ME" _TAB_TITLE: str = "CHANGE_ME" + _REDIRECT_URL: str = "CHANGE_ME" + _REDIRECT_URL_ERROR: str = "home" class Meta: abstract = True + def dispatch(self, request, *args, **kwargs): + if not self._user_has_permission(request.user): + messages.info(request, MISSING_GROUP_PERMISSION) + return redirect(reverse(self._REDIRECT_URL_ERROR)) + if not self._user_has_shared_access(request.user, **kwargs): + messages.info(request, DATA_UNSHARED) + return redirect(reverse(self._REDIRECT_URL_ERROR)) + return super().dispatch(request, *args, **kwargs) + + def _user_has_permission(self, user): + """ Has to be implemented properly by inheriting classes + + Args: + user (): + + Returns: + + """ + return False + + def _user_has_shared_access(self, user, **kwargs): + """ Has to be implemented properly by inheriting classes + + Args: + user (): + + Returns: + + """ + return False + class BaseModalFormView(BaseView): _TEMPLATE = "modal/modal_form.html" _TAB_TITLE = None + class Meta: + abstract = True + class BaseIndexView(BaseView): """ Base class for index views @@ -36,6 +72,7 @@ class BaseIndexView(BaseView): """ _TEMPLATE = "generic_index.html" _INDEX_TABLE_CLS = None + _REDIRECT_URL = "home" class Meta: abstract = True @@ -61,20 +98,22 @@ class BaseIndexView(BaseView): def _get_queryset(self): raise NotImplementedError + def _user_has_permission(self, user): + # No specific permissions needed for opening base index view + return True -class BaseIdentifierGeneratorView(View): + def _user_has_shared_access(self, user, **kwargs): + # No specific constraints for shared access of index views + return True + + +class BaseIdentifierGeneratorView(BaseView): _MODEL_CLS = None - _REDIRECT_URL_NAME: str = "home" + _REDIRECT_URL: str = "home" class Meta: abstract = True - def dispatch(self, request, *args, **kwargs): - if not self._user_has_permission(request.user): - messages.info(request, MISSING_GROUP_PERMISSION) - return redirect(reverse(self._REDIRECT_URL_NAME)) - return super().dispatch(request, *args, **kwargs) - def get(self, request: HttpRequest): tmp_obj = self._MODEL_CLS() identifier = tmp_obj.generate_new_identifier() @@ -96,3 +135,7 @@ class BaseIdentifierGeneratorView(View): """ return user.is_default_user() + + def _user_has_shared_access(self, user, **kwargs): + # No specific constraints for shared access + return True diff --git a/konova/views/deduction.py b/konova/views/deduction.py index 5158f7b0..539ab108 100644 --- a/konova/views/deduction.py +++ b/konova/views/deduction.py @@ -6,30 +6,60 @@ Created on: 22.08.22 """ from django.core.exceptions import ObjectDoesNotExist -from django.http import Http404 +from django.http import Http404, HttpRequest from django.shortcuts import get_object_or_404 from django.urls import reverse -from django.views import View from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \ RemoveEcoAccountDeductionModalForm from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_EDITED, DEDUCTION_REMOVED, DEDUCTION_UNKNOWN +from konova.views.base import BaseModalFormView -class AbstractDeductionView(View): - model = None - redirect_url = None +class AbstractDeductionView(BaseModalFormView): + _MODEL = None + _REDIRECT_URL = None def _custom_check(self, obj): """ Can be used by inheriting classes to provide custom checks before further processing """ - raise NotImplementedError("Must be implemented in subclasses") + pass + + def _user_has_permission(self, user) -> bool: + """ + + Args: + user (): + + Returns: + + """ + return user.is_default_user() + + def _user_has_shared_access(self, user, **kwargs) -> bool: + """ A user has shared access on + + Args: + user (User): The performing user + kwargs (dict): Parameters + + Returns: + bool: True if the user has access to the requested object, False otherwise + """ + ret_val: bool = False + try: + obj = self._MODEL.objects.get( + id=kwargs.get("id") + ) + ret_val = obj.is_shared_with(user) + except ObjectDoesNotExist: + ret_val = False + return ret_val class AbstractNewDeductionView(AbstractDeductionView): - class Meta: abstract = True @@ -43,13 +73,13 @@ class AbstractNewDeductionView(AbstractDeductionView): Returns: """ - obj = get_object_or_404(self.model, id=id) + obj = get_object_or_404(self._MODEL, id=id) self._custom_check(obj) form = NewEcoAccountDeductionModalForm(request.POST or None, instance=obj, request=request) return form.process_request( request, msg_success=DEDUCTION_ADDED, - redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data", + redirect_url=reverse(self._REDIRECT_URL, args=(id,)) + "#related_data", ) def post(self, request, id: str): @@ -57,10 +87,6 @@ class AbstractNewDeductionView(AbstractDeductionView): class AbstractEditDeductionView(AbstractDeductionView): - - def _custom_check(self, obj): - pass - class Meta: abstract = True @@ -75,7 +101,7 @@ class AbstractEditDeductionView(AbstractDeductionView): Returns: """ - obj = get_object_or_404(self.model, id=id) + obj = get_object_or_404(self._MODEL, id=id) self._custom_check(obj) try: eco_deduction = obj.deductions.get(id=deduction_id) @@ -87,7 +113,7 @@ class AbstractEditDeductionView(AbstractDeductionView): return form.process_request( request=request, msg_success=DEDUCTION_EDITED, - redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + redirect_url=reverse(self._REDIRECT_URL, args=(id,)) + "#related_data" ) def post(self, request, id: str, deduction_id: str): @@ -95,10 +121,6 @@ class AbstractEditDeductionView(AbstractDeductionView): class AbstractRemoveDeductionView(AbstractDeductionView): - - def _custom_check(self, obj): - pass - class Meta: abstract = True @@ -113,7 +135,7 @@ class AbstractRemoveDeductionView(AbstractDeductionView): Returns: """ - obj = get_object_or_404(self.model, id=id) + obj = get_object_or_404(self._MODEL, id=id) self._custom_check(obj) try: eco_deduction = obj.deductions.get(id=deduction_id) @@ -124,7 +146,7 @@ class AbstractRemoveDeductionView(AbstractDeductionView): return form.process_request( request=request, msg_success=DEDUCTION_REMOVED, - redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" + redirect_url=reverse(self._REDIRECT_URL, args=(id,)) + "#related_data" ) def post(self, request, id: str, deduction_id: str): diff --git a/user/views/views.py b/user/views/views.py index e639703c..953c6fe3 100644 --- a/user/views/views.py +++ b/user/views/views.py @@ -18,9 +18,17 @@ from konova.contexts import BaseContext from konova.decorators import login_required_modal -class UserDetailView(LoginRequiredMixin, BaseView): - _TAB_TITLE = _("User settings") +class UserBaseView(BaseView): + def _user_has_shared_access(self, user, **kwargs): + return True + + def _user_has_permission(self, user): + return True + + +class UserDetailView(LoginRequiredMixin, UserBaseView): _TEMPLATE = "user/index.html" + _TAB_TITLE = _("User settings") def get(self, request: HttpRequest): context = { @@ -31,7 +39,7 @@ class UserDetailView(LoginRequiredMixin, BaseView): return render(request, self._TEMPLATE, context) -class NotificationsView(LoginRequiredMixin, BaseView): +class NotificationsView(LoginRequiredMixin, UserBaseView): _TEMPLATE = "user/notifications.html" _TAB_TITLE = _("User notifications") @@ -84,6 +92,14 @@ class ContactView(LoginRequiredMixin, BaseModalFormView): context = BaseContext(request, context).context return render(request, self._TEMPLATE, context) + def _user_has_shared_access(self, user, **kwargs): + # No specific constraints + return True + + def _user_has_permission(self, user): + # No specific constraints + return True + class TeamDetailModalView(LoginRequiredMixin, BaseModalFormView): def get(self, request: HttpRequest, id: str): @@ -104,8 +120,16 @@ class TeamDetailModalView(LoginRequiredMixin, BaseModalFormView): context = BaseContext(request, context).context return render(request, self._TEMPLATE, context) + def _user_has_shared_access(self, user, **kwargs): + # No specific constraints + return True -class TeamIndexView(LoginRequiredMixin, BaseView): + def _user_has_permission(self, user): + # No specific constraints + return True + + +class TeamIndexView(LoginRequiredMixin, UserBaseView): _TEMPLATE = "user/team/index.html" _TAB_TITLE = _("Teams") -- 2.47.2 From f2baa054bf6389da790c6df559c9cc699dfe8878 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 17 Oct 2025 11:07:16 +0200 Subject: [PATCH 11/36] # Report view refactoring * refactors function based report views into class based for EIV, OEK, EMA, KOM * introduces BaseReportView for proper inheritance of shared logic * refactors generating of qr codes into proper class --- compensation/urls/compensation.py | 4 +- compensation/urls/eco_account.py | 4 +- compensation/views/compensation/report.py | 97 +++++++------------- compensation/views/eco_account/report.py | 102 ++++++--------------- ema/urls.py | 4 +- ema/views/report.py | 89 +++++------------- intervention/urls.py | 4 +- intervention/views/report.py | 87 ++++++------------ konova/utils/generators.py | 24 ----- konova/utils/qrcode.py | 47 ++++++++++ konova/views/report.py | 106 ++++++++++++++++++++++ 11 files changed, 276 insertions(+), 292 deletions(-) create mode 100644 konova/utils/qrcode.py create mode 100644 konova/views/report.py diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index d5aca228..4b74a405 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -10,7 +10,7 @@ from django.urls import path 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 CompensationReportView from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \ RemoveCompensationDeadlineView from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \ @@ -43,7 +43,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', CompensationReportView.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 c0edc4b3..1825d96a 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -12,7 +12,7 @@ from compensation.views.eco_account.eco_account import new_view, edit_view, remo detail_view, EcoAccountIndexView, 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 EcoAccountReportView from compensation.views.eco_account.resubmission import EcoAccountResubmissionView from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \ RemoveEcoAccountStateView @@ -34,7 +34,7 @@ urlpatterns = [ path('', detail_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', EcoAccountReportView.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..dde16ea0 100644 --- a/compensation/views/compensation/report.py +++ b/compensation/views/compensation/report.py @@ -5,77 +5,48 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, render from django.urls import reverse -from django.utils.translation import gettext_lazy as _ from compensation.models import Compensation -from konova.contexts import BaseContext -from konova.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.sub_settings.django_settings import BASE_URL +from konova.utils.qrcode import QrCode +from konova.views.report import BaseReportView -@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 BaseCompensationReportView(BaseReportView): + def _get_compensation_report_context(self, obj): + # Order states by surface + before_states = obj.before_states.all().order_by("-surface").prefetch_related("biotope_type") + after_states = obj.after_states.all().order_by("-surface").prefetch_related("biotope_type") + actions = obj.actions.all().prefetch_related("action_type") - Returns: - - """ - # Reuse the compensation report template since compensations are structurally identical - template = "compensation/report/compensation/report.html" - comp = get_object_or_404(Compensation, id=id) - - tab_title = _("Report {}").format(comp.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not comp.is_ready_for_publish(): - template = "report/unavailable.html" - context = { - TAB_TITLE_IDENTIFIER: tab_title, + return { + "before_states": before_states, + "after_states": after_states, + "actions": actions, } - 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) +class CompensationReportView(BaseCompensationReportView): + _MODEL = Compensation + _TEMPLATE = "compensation/report/compensation/report.html" - # 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") + def _get_report_context(self, obj): + report_url = BASE_URL + reverse("compensation:report", args=(obj.id,)) + qrcode_report = QrCode(report_url, 10) + qrcode_lanis = QrCode(obj.get_LANIS_link(), 7) - 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) + report_context = { + "qrcode": { + "img": qrcode_report.get_img(), + "url": qrcode_report.get_content(), + }, + "qrcode_lanis": { + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), + }, + "is_entry_shared": False, # disables action buttons during rendering + "tables_scrollable": False, + } + report_context.update(self._get_compensation_report_context(obj)) + return report_context \ No newline at end of file diff --git a/compensation/views/eco_account/report.py b/compensation/views/eco_account/report.py index f61a7bfc..456564da 100644 --- a/compensation/views/eco_account/report.py +++ b/compensation/views/eco_account/report.py @@ -5,85 +5,41 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, render from django.urls import reverse -from django.utils.translation import gettext_lazy as _ from compensation.models import EcoAccount -from konova.contexts import BaseContext -from konova.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 compensation.views.compensation.report import BaseCompensationReportView +from konova.sub_settings.django_settings import BASE_URL +from konova.utils.qrcode import QrCode -@uuid_required -def report_view(request: HttpRequest, id: str): - """ Renders the public report view +class EcoAccountReportView(BaseCompensationReportView): + _MODEL = EcoAccount + _TEMPLATE = "compensation/report/eco_account/report.html" - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention + def _get_report_context(self, obj): + report_url = BASE_URL + reverse("compensation:acc:report", args=(obj.id,)) + qrcode_report = QrCode(report_url, 10) + qrcode_lanis = QrCode(obj.get_LANIS_link(), 7) - Returns: + # Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier) + deductions = obj.deductions.all() \ + .distinct("intervention") \ + .select_related("intervention") \ + .values_list("intervention__id", "intervention__identifier", "intervention__title", named=True) - """ - # Reuse the compensation report template since EcoAccounts are structurally identical - template = "compensation/report/eco_account/report.html" - acc = get_object_or_404(EcoAccount, id=id) - - tab_title = _("Report {}").format(acc.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not acc.is_ready_for_publish(): - template = "report/unavailable.html" - context = { - TAB_TITLE_IDENTIFIER: tab_title, + report_context = { + "qrcode": { + "img": qrcode_report.get_img(), + "url": qrcode_report.get_content(), + }, + "qrcode_lanis": { + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), + }, + "is_entry_shared": False, # disables action buttons during rendering + "deductions": deductions, + "tables_scrollable": False, } - 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) + report_context.update(self._get_compensation_report_context(obj)) + return report_context diff --git a/ema/urls.py b/ema/urls.py index 5fad0959..f4f8f79a 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -14,7 +14,7 @@ from ema.views.ema import new_view, detail_view, edit_view, remove_view, EmaInde 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 EmaReportView 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', EmaReportView.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..8eb2a23d 100644 --- a/ema/views/report.py +++ b/ema/views/report.py @@ -5,77 +5,36 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, render from django.urls import reverse -from django.utils.translation import gettext_lazy as _ +from compensation.views.compensation.report import BaseCompensationReportView 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.sub_settings.django_settings import BASE_URL +from konova.utils.qrcode import QrCode -@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 EmaReportView(BaseCompensationReportView): + _TEMPLATE = "ema/report/report.html" + _MODEL = Ema - Returns: + def _get_report_context(self, obj): + report_url = BASE_URL + reverse("ema:report", args=(obj.id,)) + qrcode_report = QrCode(report_url, 10) + qrcode_lanis = QrCode(obj.get_LANIS_link(), 7) - """ - # Reuse the compensation report template since EMAs are structurally identical - template = "ema/report/report.html" - ema = get_object_or_404(Ema, id=id) + generic_compensation_report_context = self._get_compensation_report_context(obj) - 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, + report_context = { + "qrcode": { + "img": qrcode_report.get_img(), + "url": qrcode_report.get_content(), + }, + "qrcode_lanis": { + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), + }, + "is_entry_shared": False, # disables action buttons during rendering + "tables_scrollable": False, } - 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) + report_context.update(generic_compensation_report_context) + return report_context \ No newline at end of file diff --git a/intervention/urls.py b/intervention/urls.py index b88fdfa8..db5829fd 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -18,7 +18,7 @@ from intervention.views.intervention import new_view, detail_view, edit_view, re InterventionIndexView, InterventionIdentifierGeneratorView from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView -from intervention.views.report import report_view +from intervention.views.report import InterventionReportView from intervention.views.resubmission import InterventionResubmissionView from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \ get_revocation_view @@ -37,7 +37,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', InterventionReportView.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..2d676b1d 100644 --- a/intervention/views/report.py +++ b/intervention/views/report.py @@ -5,72 +5,41 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, render from django.urls import reverse -from django.utils.translation import gettext_lazy as _ from intervention.models import Intervention -from konova.contexts import BaseContext -from konova.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.sub_settings.django_settings import BASE_URL +from konova.utils.qrcode import QrCode +from konova.views.report import BaseReportView -@uuid_required -def report_view(request: HttpRequest, id: str): - """ Renders the public report view +class InterventionReportView(BaseReportView): + _TEMPLATE = 'intervention/report/report.html' + _MODEL = Intervention - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention + def _get_report_context(self, obj: Intervention): + """ Returns the specific context needed for an intervention report - Returns: + Args: + obj (Intervention): The object for the report - """ - template = "intervention/report/report.html" - intervention = get_object_or_404(Intervention, id=id) + Returns: + dict: The object specific context for rendering the report + """ + distinct_deductions = obj.deductions.all().distinct("account") + report_url = BASE_URL + reverse("intervention:report", args=(obj.id,)) + qrcode_report = QrCode(report_url, 10) + qrcode_lanis = QrCode(obj.get_LANIS_link(), 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 = { - TAB_TITLE_IDENTIFIER: tab_title, + return { + "deductions": distinct_deductions, + "qrcode": { + "img": qrcode_report.get_img(), + "url": qrcode_report.get_content(), + }, + "qrcode_lanis": { + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), + }, + "tables_scrollable": False, } - 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) diff --git a/konova/utils/generators.py b/konova/utils/generators.py index 78e075ad..d4aa9441 100644 --- a/konova/utils/generators.py +++ b/konova/utils/generators.py @@ -7,10 +7,6 @@ Created on: 09.11.20 """ import random import string -import qrcode -import qrcode.image.svg - -from io import BytesIO def generate_token() -> str: @@ -42,23 +38,3 @@ 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() diff --git a/konova/utils/qrcode.py b/konova/utils/qrcode.py new file mode 100644 index 00000000..75aa3a2a --- /dev/null +++ b/konova/utils/qrcode.py @@ -0,0 +1,47 @@ +""" +Author: Michel Peltriaux +Created on: 17.10.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 diff --git a/konova/views/report.py b/konova/views/report.py new file mode 100644 index 00000000..2dca0d15 --- /dev/null +++ b/konova/views/report.py @@ -0,0 +1,106 @@ +""" +Author: Michel Peltriaux +Created on: 17.10.25 + +""" +from abc import abstractmethod +from uuid import UUID + +from django.http import HttpRequest, Http404 +from django.shortcuts import get_object_or_404, render +from django.utils.translation import gettext_lazy as _ + +from konova.contexts import BaseContext +from konova.forms import SimpleGeomForm +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.views.base import BaseView + + +class BaseReportView(BaseView): + _TEMPLATE = None + _TAB_TITLE = _("Report {}") + _MODEL = None + + class Meta: + abstract = True + + def dispatch(self, request, *args, **kwargs): + # If the given id is not a uuid we act as the result was not found + try: + UUID(kwargs.get('id')) + except ValueError: + raise Http404() + return super().dispatch(request, *args, **kwargs) + + def _return_unpublishable_content_response(self, request: HttpRequest, tab_title: str): + """ Handles HttpResponse return in case the object is not ready for publish + + Args: + request (): + tab_title (): + + Returns: + + """ + template = "report/unavailable.html" + context = { + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) + + def get(self, request: HttpRequest, id: str): + """ Renders the public report view + + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention + + Returns: + + """ + obj = get_object_or_404(self._MODEL, id=id) + tab_title = self._TAB_TITLE.format(obj.identifier) + + # If object is not recorded we need to render another template without any data + if not obj.is_ready_for_publish(): + return self._return_unpublishable_content_response(request, tab_title) + + # First get specific report context for different types of objects due to inheritance + report_context = self._get_report_context(obj) + + # Then generate and add default report context (the same for all models) + geom_form = SimpleGeomForm(instance=obj) + parcels = obj.get_underlying_parcels() + report_context.update( + { + TAB_TITLE_IDENTIFIER: tab_title, + "parcels": parcels, + "geom_form": geom_form, + "obj": obj + } + ) + + # Then generate the general context based on the report specific data + context = BaseContext(request, report_context).context + return render(request, self._TEMPLATE, context) + + @abstractmethod + def _get_report_context(self, obj): + """ Returns the specific context needed for this report view + + Args: + obj (RecordableObjectMixin): The object for the report + + Returns: + dict: The object specific context for rendering the report + """ + raise NotImplementedError + + def _user_has_permission(self, user): + # Reports do not need specific permissions to be callable + return True + + def _user_has_shared_access(self, user, **kwargs): + # Reports do not need specific share states to be callable + return True -- 2.47.2 From 61ec9c8c9b183523497fbd90c9c3546036c62c00 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 17 Oct 2025 14:42:44 +0200 Subject: [PATCH 12/36] # ClientProxyView * refactors login required from method decorator to mixin inheritance --- konova/views/map_proxy.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/konova/views/map_proxy.py b/konova/views/map_proxy.py index daadb3c2..e702ad28 100644 --- a/konova/views/map_proxy.py +++ b/konova/views/map_proxy.py @@ -10,9 +10,8 @@ from json import JSONDecodeError import requests import urllib3.util -from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.http import JsonResponse, HttpRequest, HttpResponse -from django.utils.decorators import method_decorator from django.utils.http import urlencode from django.views import View @@ -22,17 +21,13 @@ from konova.sub_settings.lanis_settings import MAP_PROXY_HOST_WHITELIST from konova.sub_settings.proxy_settings import PROXIES, GEOPORTAL_RLP_USER, GEOPORTAL_RLP_PASSWORD -class BaseClientProxyView(View): +class BaseClientProxyView(LoginRequiredMixin, View): """ Provides proxy functionality for NETGIS map client. """ class Meta: abstract = True - @method_decorator(login_required) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) - def _check_with_whitelist(self, url): parsed_url = urllib3.util.parse_url(url) parsed_url_host = parsed_url.host @@ -67,7 +62,6 @@ class BaseClientProxyView(View): class ClientProxyParcelSearch(BaseClientProxyView): - def get(self, request: HttpRequest): url = request.META.get("QUERY_STRING") -- 2.47.2 From a9b402862b91de708ac1c1094df5afcd57c003e9 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 17 Oct 2025 15:40:45 +0200 Subject: [PATCH 13/36] # Detail View * introduces BaseDetailView * refactors detail views for EIV, KOM, OEK, EMA from function based to class based * refactors already class based HomeView to inherit from new BaseView --- compensation/urls/compensation.py | 6 +- compensation/urls/eco_account.py | 4 +- .../views/compensation/compensation.py | 138 ++++++++--------- compensation/views/eco_account/eco_account.py | 144 ++++++++---------- ema/urls.py | 6 +- ema/views/ema.py | 93 +++++------ intervention/urls.py | 6 +- intervention/views/intervention.py | 108 ++++++------- konova/utils/general.py | 13 +- konova/views/base.py | 8 +- konova/views/detail.py | 107 +++++++++++++ konova/views/home.py | 18 ++- 12 files changed, 357 insertions(+), 294 deletions(-) create mode 100644 konova/views/detail.py diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index 4b74a405..adec4b7f 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -17,8 +17,8 @@ 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, detail_view, edit_view, \ - remove_view, CompensationIndexView, CompensationIdentifierGeneratorView +from compensation.views.compensation.compensation import new_view, edit_view, \ + remove_view, CompensationIndexView, CompensationIdentifierGeneratorView, CompensationDetailView from compensation.views.compensation.log import CompensationLogView urlpatterns = [ @@ -27,7 +27,7 @@ urlpatterns = [ path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'), path('new/', new_view, name='new'), path('new', new_view, name='new'), - path('', detail_view, name='detail'), + path('', CompensationDetailView.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 1825d96a..f4989be7 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -9,7 +9,7 @@ from django.urls import path from compensation.autocomplete.eco_account import EcoAccountAutocomplete from compensation.views.eco_account.eco_account import new_view, edit_view, remove_view, \ - detail_view, EcoAccountIndexView, EcoAccountIdentifierGeneratorView + EcoAccountIndexView, EcoAccountIdentifierGeneratorView, EcoAccountDetailView from compensation.views.eco_account.log import EcoAccountLogView from compensation.views.eco_account.record import EcoAccountRecordView from compensation.views.eco_account.report import EcoAccountReportView @@ -31,7 +31,7 @@ urlpatterns = [ path("", EcoAccountIndexView.as_view(), name="index"), path('new/', new_view, name='new'), path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'), - path('', detail_view, name='detail'), + path('', EcoAccountDetailView.as_view(), name='detail'), path('/log', EcoAccountLogView.as_view(), name='log'), path('/record', EcoAccountRecordView.as_view(), name='record'), path('/report', EcoAccountReportView.as_view(), name='report'), diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 749a88aa..2403d2b8 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -19,16 +19,15 @@ 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, \ 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.base import BaseIndexView, BaseIdentifierGeneratorView +from konova.views.detail import BaseDetailView class CompensationIndexView(LoginRequiredMixin, BaseIndexView): @@ -185,82 +184,71 @@ 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 +class CompensationDetailView(BaseDetailView): + _MODEL_CLS = Compensation + _TEMPLATE = "compensation/detail/compensation/view.html" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id + def _get_object(self, id: str): + """ Returns the compensation - Returns: + Args: + id (str): The compensation's id - """ - 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 + Returns: + obj (Compensation): The compensation + """ + comp = get_object_or_404( + Compensation.objects.select_related( + "modified", + "created", + "geometry" + ), + id=id, + deleted=None, + intervention__deleted=None, ) + return comp - 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) + def _get_detail_context(self, obj: Compensation): + """ Generate object specific detail context for view + + Args: + obj (): The record + + Returns: + + """ + # Order states according to surface + before_states = obj.before_states.all().prefetch_related("biotope_type").order_by("-surface") + after_states = obj.after_states.all().prefetch_related("biotope_type").order_by("-surface") + actions = obj.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 = obj.get_surface_before_states() + sum_after_states = obj.get_surface_after_states() + diff_states = abs(sum_before_states - sum_after_states) + + last_checked = obj.intervention.get_last_checked_action() + last_checked_tooltip = "" + if last_checked: + last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format( + last_checked.get_timestamp_str_formatted(), + last_checked.user + ) + + context = { + "last_checked": last_checked, + "last_checked_tooltip": last_checked_tooltip, + "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, + "has_finished_deadlines": obj.get_finished_deadlines().exists(), + } + return context @login_required_modal diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 55a56a2e..42eb569e 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.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.contrib.auth.mixins import LoginRequiredMixin -from django.http import HttpRequest, JsonResponse +from django.http import HttpRequest from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -17,14 +17,14 @@ 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.base import BaseIndexView, BaseIdentifierGeneratorView +from konova.views.detail import BaseDetailView class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView): @@ -162,86 +162,72 @@ 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 +class EcoAccountDetailView(BaseDetailView): + _MODEL_CLS = EcoAccount + _TEMPLATE = "compensation/detail/eco_account/view.html" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id + def _get_object(self, id: str): + """ Fetch object for detail view - Returns: + Args: + id (str): The record's id' - """ - 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) + Returns: - # 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 + """ + acc = get_object_or_404( + EcoAccount.objects.prefetch_related( + "deadlines", + ).select_related( + 'geometry', + 'responsible', + ), + id=id, + deleted=None, ) + return acc - 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) + def _get_detail_context(self, obj: EcoAccount): + """ Generate object specific detail context for view + + Args: + obj (): The record + + Returns: + + """ + # Order states according to surface + before_states = obj.before_states.order_by("-surface") + after_states = obj.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 = obj.get_surface_before_states() + sum_after_states = obj.get_surface_after_states() + diff_states = abs(sum_before_states - sum_after_states) + # Calculate rest of available surface for deductions + available_total = obj.deductable_rest + available_relative = obj.get_deductable_rest_relative() + + # Prefetch related data to decrease the amount of db connections + deductions = obj.deductions.filter( + intervention__deleted=None, + ) + actions = obj.actions.all() + + context = { + "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, + "deductions": deductions, + "actions": actions, + "has_finished_deadlines": obj.get_finished_deadlines().exists(), + } + return context @login_required_modal diff --git a/ema/urls.py b/ema/urls.py index f4f8f79a..3469ed13 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -10,8 +10,8 @@ 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 new_view, detail_view, edit_view, remove_view, EmaIndexView, \ - EmaIdentifierGeneratorView +from ema.views.ema import new_view, edit_view, remove_view, EmaIndexView, \ + EmaIdentifierGeneratorView, EmaDetailView from ema.views.log import EmaLogView from ema.views.record import EmaRecordView from ema.views.report import EmaReportView @@ -24,7 +24,7 @@ urlpatterns = [ path("", EmaIndexView.as_view(), name="index"), path("new/", new_view, name="new"), path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"), - path("", detail_view, name="detail"), + path("", EmaDetailView.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/ema.py b/ema/views/ema.py index 8ec9a8b1..d0e04126 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.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.contrib.auth.mixins import LoginRequiredMixin -from django.http import HttpRequest, JsonResponse +from django.http import HttpRequest from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -17,15 +17,14 @@ 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, MISSING_GROUP_PERMISSION + GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView +from konova.views.detail import BaseDetailView class EmaIndexView(LoginRequiredMixin, BaseIndexView): @@ -104,64 +103,50 @@ class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView return user.is_ets_user() -@login_required -@uuid_required -def detail_view(request: HttpRequest, id: str): - """ Renders the detail view of an EMA +class EmaDetailView(BaseDetailView): + _MODEL_CLS = Ema + _TEMPLATE = "ema/detail/view.html" - Args: - request (HttpRequest): The incoming request - id (str): The EMA id + def _get_object(self, id: str): + """ Fetch object for detail view - Returns: + Args: + id (str): The record's id' - """ - template = "ema/detail/view.html" - ema = get_object_or_404(Ema, id=id, deleted=None) + Returns: - geom_form = SimpleGeomForm(instance=ema) - parcels = ema.get_underlying_parcels() - _user = request.user - is_entry_shared = ema.is_shared_with(_user) + """ + ema = get_object_or_404(Ema, id=id, deleted=None) + return ema - # Order states according to surface - before_states = ema.before_states.all().order_by("-surface") - after_states = ema.after_states.all().order_by("-surface") + def _get_detail_context(self, obj: Ema): + """ Generate object specific detail context for view - # 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) + Args: + obj (): The record - ema.set_status_messages(request) + Returns: - 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 - ) + """ + # Order states according to surface + before_states = obj.before_states.all().order_by("-surface") + after_states = obj.after_states.all().order_by("-surface") - 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) + # 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 = obj.get_surface_before_states() + sum_after_states = obj.get_surface_after_states() + diff_states = abs(sum_before_states - sum_after_states) + + context = { + "before_states": before_states, + "after_states": after_states, + "sum_before_states": sum_before_states, + "sum_after_states": sum_after_states, + "diff_states": diff_states, + "has_finished_deadlines": obj.get_finished_deadlines().exists(), + } + return context @login_required diff --git a/intervention/urls.py b/intervention/urls.py index db5829fd..3a21df5f 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, detail_view, edit_view, remove_view, \ - InterventionIndexView, InterventionIdentifierGeneratorView +from intervention.views.intervention import new_view, edit_view, remove_view, \ + InterventionIndexView, InterventionIdentifierGeneratorView, InterventionDetailView from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView from intervention.views.report import InterventionReportView @@ -29,7 +29,7 @@ urlpatterns = [ path("", InterventionIndexView.as_view(), name="index"), path('new/', new_view, name='new'), path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'), - path('', detail_view, name='detail'), + path('', InterventionDetailView.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 80a92b51..e9d297f7 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -27,6 +27,7 @@ from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, REC CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \ GEOMETRIES_IGNORED_TEMPLATE from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView +from konova.views.detail import BaseDetailView class InterventionIndexView(LoginRequiredMixin, BaseIndexView): @@ -105,78 +106,59 @@ class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGene _REDIRECT_URL = "intervention:index" -@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 InterventionDetailView(BaseDetailView): + _MODEL_CLS = Intervention + _TEMPLATE = "intervention/detail/view.html" - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id + def _get_object(self, id: str): + """ Returns the intervention - Returns: + Args: + id (str): The intervention's id - """ - 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 + Returns: + obj (Intervention): The intervention + """ + # Fetch data, filter out deleted related data + obj = get_object_or_404( + self._MODEL_CLS.objects.select_related( + "geometry", + "legal", + "responsible", + ).prefetch_related( + "legal__revocations", + ), + id=id, + deleted=None ) + return obj - has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists() + def _get_detail_context(self, obj: Intervention): + """ Generate object specific detail context for view - 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 - ) + Args: + obj (): The record - 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}", - } + Returns: - request = intervention.set_status_messages(request) + """ + compensations = obj.compensations.filter(deleted=None) + last_checked = obj.get_last_checked_action() + last_checked_tooltip = "" + if last_checked: + last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format( + last_checked.get_timestamp_str_formatted(), + last_checked.user + ) - context = BaseContext(request, context).context - return render(request, template, context) + has_payment_without_document = obj.payments.exists() and not obj.get_documents()[1].exists() + context = { + "last_checked": last_checked, + "last_checked_tooltip": last_checked_tooltip, + "compensations": compensations, + "has_payment_without_document": has_payment_without_document, + } + return context @login_required diff --git a/konova/utils/general.py b/konova/utils/general.py index 43341ffd..666d4811 100644 --- a/konova/utils/general.py +++ b/konova/utils/general.py @@ -5,9 +5,11 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 17.09.21 """ +from uuid import UUID + from django.contrib import messages from django.utils.translation import gettext_lazy as _ -from django.http import HttpRequest +from django.http import HttpRequest, Http404 def format_german_float(num) -> str: @@ -38,3 +40,12 @@ def check_user_is_in_any_group(request: HttpRequest): _("+++ Attention: You are not part of any group. You won't be able to create, edit or do anything. Please contact an administrator. +++") ) return request + +def check_id_is_valid_uuid(**kwargs: dict): + uuid = kwargs.get("uuid", None) or kwargs.get("id", None) + if uuid: + try: + # Check whether the id is a proper uuid or something that would break a db fetch + UUID(uuid) + except ValueError: + raise Http404 diff --git a/konova/views/base.py b/konova/views/base.py index 9a6099c1..74a0bdd5 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -27,12 +27,16 @@ class BaseView(View): abstract = True def dispatch(self, request, *args, **kwargs): + request = check_user_is_in_any_group(request) + if not self._user_has_permission(request.user): messages.info(request, MISSING_GROUP_PERMISSION) return redirect(reverse(self._REDIRECT_URL_ERROR)) + if not self._user_has_shared_access(request.user, **kwargs): messages.info(request, DATA_UNSHARED) return redirect(reverse(self._REDIRECT_URL_ERROR)) + return super().dispatch(request, *args, **kwargs) def _user_has_permission(self, user): @@ -77,10 +81,6 @@ class BaseIndexView(BaseView): class Meta: abstract = True - def dispatch(self, request, *args, **kwargs): - request = check_user_is_in_any_group(request) - return super().dispatch(request, *args, **kwargs) - def get(self, request: HttpRequest): qs = self._get_queryset() table = self._INDEX_TABLE_CLS( diff --git a/konova/views/detail.py b/konova/views/detail.py new file mode 100644 index 00000000..6322ab9b --- /dev/null +++ b/konova/views/detail.py @@ -0,0 +1,107 @@ +""" +Author: Michel Peltriaux +Created on: 17.10.25 + +""" +from abc import abstractmethod + +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest +from django.shortcuts import render + +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.general import check_id_is_valid_uuid +from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE +from konova.views.base import BaseView + + +class BaseDetailView(LoginRequiredMixin, BaseView): + _MODEL_CLS = None + + class Meta: + abstract = True + + def dispatch(self, request, *args, **kwargs): + check_id_is_valid_uuid(**kwargs) + return super().dispatch(request, *args, **kwargs) + + def _user_has_shared_access(self, user, **kwargs): + """ Check if user has shared access to this object + + Args: + user (): + **kwargs (): + + Returns: + + """ + # Access to an entry's detail view is not restricted by the state of being-shared or not + return True + + def _user_has_permission(self, user): + # Detail views have no restrictions + return True + + def get(self, request: HttpRequest, id: str): + """ Get endpoint for detail view + + Args: + request (HttpRequest): The incoming request + id (str): The record's id + + Returns: + + """ + obj = self._get_object(id) + geom_form = SimpleGeomForm(instance=obj) + user = request.user + + requesting_user_is_only_shared_user = obj.is_only_shared_with(user) + if requesting_user_is_only_shared_user: + messages.info(request, DO_NOT_FORGET_TO_SHARE) + + obj.set_status_messages(request) + + detail_context = self._get_detail_context(obj) + context = BaseContext(request, detail_context).context + context.update( + { + "obj": obj, + "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": obj.get_LANIS_link(), + "is_entry_shared": obj.is_shared_with(user=user), + TAB_TITLE_IDENTIFIER: f"{obj.identifier} - {obj.title}" + } + ) + return render(request,self._TEMPLATE, context) + + @abstractmethod + def _get_detail_context(self, obj): + """ Generate object specific detail context for view + + Args: + obj (): The record + + Returns: + + """ + raise NotImplementedError + + @abstractmethod + def _get_object(self, id: str): + """ Fetch object for detail view + + Args: + id (str): The record's id' + + Returns: + + """ + raise NotImplementedError diff --git a/konova/views/home.py b/konova/views/home.py index 5253bbfe..35cbcb1b 100644 --- a/konova/views/home.py +++ b/konova/views/home.py @@ -9,21 +9,19 @@ from django.contrib.auth.mixins import LoginRequiredMixin from django.db.models import Q from django.http import HttpRequest from django.shortcuts import render -from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ -from django.views import View from compensation.models import EcoAccount, Compensation from intervention.models import Intervention from konova.contexts import BaseContext -from konova.decorators import any_group_check from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.views.base import BaseView from news.models import ServerMessage -class HomeView(LoginRequiredMixin, View): +class HomeView(LoginRequiredMixin, BaseView): + _TEMPLATE = "konova/home.html" - @method_decorator(any_group_check) def get(self, request: HttpRequest): """ Renders the landing page @@ -34,7 +32,6 @@ class HomeView(LoginRequiredMixin, View): Returns: A redirect """ - template = "konova/home.html" user = request.user user_teams = user.shared_teams @@ -75,5 +72,12 @@ class HomeView(LoginRequiredMixin, View): TAB_TITLE_IDENTIFIER: _("Home"), } context = BaseContext(request, additional_context).context - return render(request, template, context) + return render(request, self._TEMPLATE, context) + def _user_has_permission(self, user): + # No specific permission needed for home view + return True + + def _user_has_shared_access(self, user, **kwargs): + # No specific constraint needed for home view + return True -- 2.47.2 From d03b714fb5280dab2b74adc048847c08ad9a8e4f Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Sun, 19 Oct 2025 12:37:13 +0200 Subject: [PATCH 14/36] # NewIntervention view * introduces BaseFormView and BaseNewSpatialLocatedObjectFormView * refactors new intervention view from function to class --- intervention/urls.py | 6 +- intervention/views/intervention.py | 71 +++-------------- konova/forms/base_form.py | 2 + konova/views/base.py | 119 ++++++++++++++++++++++++++++- 4 files changed, 132 insertions(+), 66 deletions(-) diff --git a/intervention/urls.py b/intervention/urls.py index 3a21df5f..4b701dca 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, remove_view, \ - InterventionIndexView, InterventionIdentifierGeneratorView, InterventionDetailView +from intervention.views.intervention import edit_view, remove_view, \ + InterventionIndexView, InterventionIdentifierGeneratorView, InterventionDetailView, NewInterventionFormView from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView from intervention.views.report import InterventionReportView @@ -27,7 +27,7 @@ from intervention.views.share import InterventionShareFormView, InterventionShar app_name = "intervention" urlpatterns = [ path("", InterventionIndexView.as_view(), name="index"), - path('new/', new_view, name='new'), + path('new/', NewInterventionFormView.as_view(), name='new'), path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'), path('', InterventionDetailView.as_view(), name='detail'), path('/log', InterventionLogView.as_view(), name='log'), diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index e9d297f7..f45d55a0 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.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.contrib.auth.mixins import LoginRequiredMixin -from django.http import JsonResponse, HttpRequest +from django.http import HttpRequest from django.shortcuts import get_object_or_404, render, redirect from django.urls import reverse from django.utils.translation import gettext_lazy as _ @@ -17,16 +17,13 @@ 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, \ - GEOMETRIES_IGNORED_TEMPLATE -from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView + CHECK_STATE_RESET, FORM_INVALID, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE +from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView from konova.views.detail import BaseDetailView @@ -45,60 +42,12 @@ class InterventionIndexView(LoginRequiredMixin, BaseIndexView): return qs -@login_required -@default_group_required -def new_view(request: HttpRequest): - """ - Renders a view for a new intervention creation - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "intervention/form/view.html" - data_form = NewInterventionForm(request.POST or None) - geom_form = SimpleGeomForm(request.POST or None, read_only=False) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - generated_identifier = data_form.cleaned_data.get("identifier", None) - intervention = data_form.save(request.user, geom_form) - if generated_identifier != intervention.identifier: - messages.info( - request, - IDENTIFIER_REPLACED.format( - generated_identifier, - intervention.identifier - ) - ) - messages.success(request, _("Intervention {} added").format(intervention.identifier)) - if geom_form.has_geometry_simplified(): - messages.info( - 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: _("New intervention"), - } - context = BaseContext(request, context).context - return render(request, template, context) +class NewInterventionFormView(BaseNewSpatialLocatedObjectFormView): + _MODEL_CLS = Intervention + _FORM_CLS = NewInterventionForm + _TEMPLATE = "intervention/form/view.html" + _REDIRECT_URL = "intervention:detail" + _TAB_TITLE = _("New intervention") class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): diff --git a/konova/forms/base_form.py b/konova/forms/base_form.py index 98eaa41c..337f1a75 100644 --- a/konova/forms/base_form.py +++ b/konova/forms/base_form.py @@ -25,6 +25,7 @@ class BaseForm(forms.Form): cancel_redirect = None form_caption = None instance = None # The data holding model object + user = None # The performing user request = None form_attrs = {} # Holds additional attributes, that can be used in the template has_required_fields = False # Automatically set. Triggers hint rendering in templates @@ -33,6 +34,7 @@ class BaseForm(forms.Form): def __init__(self, *args, **kwargs): self.instance = kwargs.pop("instance", None) + self.user = kwargs.pop("user", None) super().__init__(*args, **kwargs) if self.request is not None: self.user = self.request.user diff --git a/konova/views/base.py b/konova/views/base.py index 74a0bdd5..545c400b 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -6,15 +6,19 @@ Created on: 15.10.25 from abc import abstractmethod from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest, JsonResponse -from django.shortcuts import render, redirect +from django.shortcuts import render, redirect, get_object_or_404 from django.urls import reverse from django.views import View +from django.utils.translation import gettext_lazy as _ from konova.contexts import BaseContext +from konova.forms import BaseForm, SimpleGeomForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.general import check_user_is_in_any_group -from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED +from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED, IDENTIFIER_REPLACED, \ + GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE class BaseView(View): @@ -139,3 +143,114 @@ class BaseIdentifierGeneratorView(BaseView): def _user_has_shared_access(self, user, **kwargs): # No specific constraints for shared access return True + + +class BaseFormView(BaseView): + _MODEL_CLS = None + _FORM_CLS = None + + class Meta: + abstract = True + + def _get_specific_context_data(self, **kwargs): + return {} + + +class BaseNewSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView): + _GEOMETRY_FORM_CLS = SimpleGeomForm + + def _user_has_permission(self, user): + # User has to have default privilege to call this endpoint + return user.is_default_user() + + def _user_has_shared_access(self, user, **kwargs): + # There is no shared access control since nothing exists yet + return True + + def get(self, request: HttpRequest): + form: BaseForm = self._FORM_CLS(None, user=request.user) + geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, user=request.user, read_only=False) + + context = self._get_specific_context_data() + context = BaseContext(request, additional_context=context).context + context.update( + { + "form": form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + ) + return render(request, self._TEMPLATE, context) + + def post(self, request: HttpRequest): + form: BaseForm = self._FORM_CLS(request.POST or None, user=request.user) + geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(request.POST or None, user=request.user, read_only=False) + + if form.is_valid() and geom_form.is_valid(): + obj = form.save(request.user, geom_form) + self._REDIRECT_URL = reverse(self._REDIRECT_URL, args=(obj.id,)) + + generated_identifier = form.cleaned_data.get("identifier", None) + + if generated_identifier != obj.identifier: + messages.info( + request, + IDENTIFIER_REPLACED.format( + generated_identifier, + obj.identifier + ) + ) + messages.success(request, _("{} added").format(obj.identifier)) + if geom_form.has_geometry_simplified(): + messages.info( + 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(self._REDIRECT_URL) + else: + context = self._get_specific_context_data() + + context = BaseContext(request, additional_context=context).context + return render(request, self._TEMPLATE, context) + + +class BaseEditSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView): + def get(self, request: HttpRequest, id: str): + obj = get_object_or_404( + self._MODEL_CLS, + id=id + ) + form: BaseForm = self._FORM_CLS(None, instance=obj, user=request.user) + context = self._get_specific_context_data() + context = BaseContext(request, additional_context=context).context + context.update( + { + "form": form, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + ) + return render(request, self._TEMPLATE, context) + + def post(self, request: HttpRequest, id: str): + obj = get_object_or_404( + self._MODEL_CLS, + id=id + ) + form: BaseForm = self._FORM_CLS(request.POST or None, instance=obj, user=request.user) + + if form.is_valid(): + obj = form.save() + context = self._get_specific_context_data(obj=obj) + else: + context = self._get_specific_context_data() + + context = BaseContext(request, additional_context=context).context + return render(request, self._TEMPLATE, context) -- 2.47.2 From 9e4a78ec60926a52dc4e63b511a3b0d3ce6a3742 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Sun, 19 Oct 2025 12:50:35 +0200 Subject: [PATCH 15/36] # EditIntervention view * refactors edit intervention view from function to class --- intervention/views/intervention.py | 18 ++++++- konova/views/base.py | 75 +++++++++++++++++++++++++----- 2 files changed, 80 insertions(+), 13 deletions(-) diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index f45d55a0..75e15279 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -23,7 +23,8 @@ from konova.forms.modals import RemoveModalForm 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, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE -from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView +from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ + BaseEditSpatialLocatedObjectFormView from konova.views.detail import BaseDetailView @@ -50,6 +51,21 @@ class NewInterventionFormView(BaseNewSpatialLocatedObjectFormView): _TAB_TITLE = _("New intervention") +class EditInterventionFormView(BaseEditSpatialLocatedObjectFormView): + _MODEL_CLS = Intervention + _FORM_CLS = EditInterventionForm + _TEMPLATE = "intervention/form/view.html" + _REDIRECT_URL = "intervention:detail" + _TAB_TITLE = _("Edit {}") + + def _user_has_shared_access(self, user, **kwargs): + obj = get_object_or_404(self._REDIRECT_URL, id=kwargs.get('id', None)) + return obj.is_shared_with(user) + + def _user_has_permission(self, user): + return user.is_default_user() + + class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): _MODEL_CLS = Intervention _REDIRECT_URL = "intervention:index" diff --git a/konova/views/base.py b/konova/views/base.py index 545c400b..a3ca4716 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -18,7 +18,7 @@ from konova.forms import BaseForm, SimpleGeomForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.general import check_user_is_in_any_group from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED, IDENTIFIER_REPLACED, \ - GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE + GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET class BaseView(View): @@ -152,13 +152,19 @@ class BaseFormView(BaseView): class Meta: abstract = True - def _get_specific_context_data(self, **kwargs): + def _get_additional_context_data(self, **kwargs): return {} -class BaseNewSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView): +class BaseSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView): _GEOMETRY_FORM_CLS = SimpleGeomForm + class Meta: + abstract = True + + +class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): + def _user_has_permission(self, user): # User has to have default privilege to call this endpoint return user.is_default_user() @@ -171,7 +177,7 @@ class BaseNewSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView): form: BaseForm = self._FORM_CLS(None, user=request.user) geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, user=request.user, read_only=False) - context = self._get_specific_context_data() + context = self._get_additional_context_data() context = BaseContext(request, additional_context=context).context context.update( { @@ -216,24 +222,36 @@ class BaseNewSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView): return redirect(self._REDIRECT_URL) else: - context = self._get_specific_context_data() + context = self._get_additional_context_data() context = BaseContext(request, additional_context=context).context return render(request, self._TEMPLATE, context) -class BaseEditSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView): +class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): def get(self, request: HttpRequest, id: str): obj = get_object_or_404( self._MODEL_CLS, id=id ) + self._REDIRECT_URL = reverse(self._REDIRECT_URL, args=(obj.id,)) + + if obj.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect(self._REDIRECT_URL) + form: BaseForm = self._FORM_CLS(None, instance=obj, user=request.user) - context = self._get_specific_context_data() + geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, instance=obj, read_only=False) + + context = self._get_additional_context_data() context = BaseContext(request, additional_context=context).context context.update( { "form": form, + "geom_form": geom_form, TAB_TITLE_IDENTIFIER: self._TAB_TITLE, } ) @@ -244,13 +262,46 @@ class BaseEditSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView): self._MODEL_CLS, id=id ) - form: BaseForm = self._FORM_CLS(request.POST or None, instance=obj, user=request.user) + self._REDIRECT_URL = reverse(self._REDIRECT_URL, args=(obj.id,)) - if form.is_valid(): - obj = form.save() - context = self._get_specific_context_data(obj=obj) + form: BaseForm = self._FORM_CLS(request.POST or None, instance=obj, user=request.user) + geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, instance=obj, read_only=False) + + if form.is_valid() and geom_form.is_valid(): + obj = form.save(request.user, geom_form) + self._REDIRECT_URL = reverse(self._REDIRECT_URL, args=(obj.id,)) + + messages.success(request, _("{} edited").format(obj.identifier)) + + # 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 + obj_is_checked = obj.checked is not None + if obj_is_checked: + messages.info(request, CHECK_STATE_RESET) + + if geom_form.has_geometry_simplified(): + messages.info( + 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(self._REDIRECT_URL) else: - context = self._get_specific_context_data() + context = self._get_additional_context_data() context = BaseContext(request, additional_context=context).context + context.update( + { + "form": form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE.format(obj.identifier), + } + ) return render(request, self._TEMPLATE, context) -- 2.47.2 From 278a951e92bfdcd16f9314c761941a29a3d2242d Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Sun, 19 Oct 2025 13:10:22 +0200 Subject: [PATCH 16/36] # NewEma EditEma views * refactors views for new ema and edit ema from function to class based * moves shared access check to base edit form view to be checked for every inheriting class * fixes bug where private variables changed on singleton objects * updates translations --- ema/urls.py | 8 +- ema/views/ema.py | 141 +++++------------------------ intervention/urls.py | 7 +- intervention/views/intervention.py | 7 -- konova/views/base.py | 23 +++-- locale/de/LC_MESSAGES/django.mo | Bin 46187 -> 46205 bytes locale/de/LC_MESSAGES/django.po | 102 +++++++++++---------- 7 files changed, 100 insertions(+), 188 deletions(-) diff --git a/ema/urls.py b/ema/urls.py index 3469ed13..bfc6c0f4 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -10,8 +10,8 @@ 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 new_view, edit_view, remove_view, EmaIndexView, \ - EmaIdentifierGeneratorView, EmaDetailView +from ema.views.ema import remove_view, EmaIndexView, \ + EmaIdentifierGeneratorView, EmaDetailView, EditEmaFormView, NewEmaFormView from ema.views.log import EmaLogView from ema.views.record import EmaRecordView from ema.views.report import EmaReportView @@ -22,11 +22,11 @@ from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateVie app_name = "ema" urlpatterns = [ path("", EmaIndexView.as_view(), name="index"), - path("new/", new_view, name="new"), + path("new/", NewEmaFormView.as_view(), name="new"), path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"), path("", EmaDetailView.as_view(), name="detail"), path('/log', EmaLogView.as_view(), name='log'), - path('/edit', edit_view, name='edit'), + path('/edit', EditEmaFormView.as_view(), name='edit'), path('/remove', remove_view, name='remove'), path('/record', EmaRecordView.as_view(), name='record'), path('/report', EmaReportView.as_view(), name='report'), diff --git a/ema/views/ema.py b/ema/views/ema.py index d0e04126..4a287ab9 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -5,25 +5,20 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib import messages from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest -from django.shortcuts import get_object_or_404, redirect, render +from django.shortcuts import get_object_or_404 from django.urls import reverse from django.utils.translation import gettext_lazy as _ from ema.forms import NewEmaForm, EditEmaForm from ema.models import Ema from ema.tables import EmaTable -from konova.contexts import BaseContext from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal -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 -from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView +from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ + BaseEditSpatialLocatedObjectFormView from konova.views.detail import BaseDetailView @@ -40,59 +35,32 @@ class EmaIndexView(LoginRequiredMixin, BaseIndexView): return qs -@login_required -@conservation_office_group_required -def new_view(request: HttpRequest): - """ - Renders a view for a new eco account creation +class NewEmaFormView(BaseNewSpatialLocatedObjectFormView): + _FORM_CLS = NewEmaForm + _MODEL_CLS = Ema + _TEMPLATE = "ema/form/view.html" + _TAB_TITLE = _("New EMA") + _REDIRECT_URL = "ema:detail" - Args: - request (HttpRequest): The incoming request + def _user_has_permission(self, user): + # User has to be an ets user + return user.is_ets_user() - Returns: + def _user_has_shared_access(self, user, **kwargs): + # No specific share constraint for creatin EMA entries + return True - """ - template = "ema/form/view.html" - data_form = NewEmaForm(request.POST or None) - geom_form = SimpleGeomForm(request.POST or None, read_only=False) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - generated_identifier = data_form.cleaned_data.get("identifier", None) - ema = data_form.save(request.user, geom_form) - if generated_identifier != ema.identifier: - messages.info( - request, - IDENTIFIER_REPLACED.format( - generated_identifier, - ema.identifier - ) - ) - messages.success(request, _("EMA {} added").format(ema.identifier)) - if geom_form.has_geometry_simplified(): - messages.info( - 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: _("New EMA"), - } - context = BaseContext(request, context).context - return render(request, template, context) +class EditEmaFormView(BaseEditSpatialLocatedObjectFormView): + _MODEL_CLS = Ema + _FORM_CLS = EditEmaForm + _TEMPLATE = "ema/form/view.html" + _REDIRECT_URL = "ema:detail" + _TAB_TITLE = _("Edit {}") + + def _user_has_permission(self, user): + # User has to be an ets user + return user.is_ets_user() class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): @@ -149,65 +117,6 @@ class EmaDetailView(BaseDetailView): return context -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing compensations - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "compensation/form/view.html" - # Get object from db - ema = get_object_or_404(Ema, id=id) - if ema.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("ema:detail", id=id) - - # Create forms, initialize with values from db/from POST request - data_form = EditEmaForm(request.POST or None, instance=ema) - geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - # The data form takes the geom form for processing, as well as the performing user - ema = data_form.save(request.user, geom_form) - messages.success(request, _("EMA {} edited").format(ema.identifier)) - if geom_form.has_geometry_simplified(): - messages.info( - 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) - - @login_required_modal @login_required @conservation_office_group_required diff --git a/intervention/urls.py b/intervention/urls.py index 4b701dca..dae2d47d 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 edit_view, remove_view, \ - InterventionIndexView, InterventionIdentifierGeneratorView, InterventionDetailView, NewInterventionFormView +from intervention.views.intervention import remove_view, \ + InterventionIndexView, InterventionIdentifierGeneratorView, InterventionDetailView, NewInterventionFormView, \ + EditInterventionFormView from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView from intervention.views.report import InterventionReportView @@ -31,7 +32,7 @@ urlpatterns = [ path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'), path('', InterventionDetailView.as_view(), name='detail'), path('/log', InterventionLogView.as_view(), name='log'), - path('/edit', edit_view, name='edit'), + path('/edit', EditInterventionFormView.as_view(), name='edit'), path('/remove', remove_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 75e15279..cd0e9308 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -58,13 +58,6 @@ class EditInterventionFormView(BaseEditSpatialLocatedObjectFormView): _REDIRECT_URL = "intervention:detail" _TAB_TITLE = _("Edit {}") - def _user_has_shared_access(self, user, **kwargs): - obj = get_object_or_404(self._REDIRECT_URL, id=kwargs.get('id', None)) - return obj.is_shared_with(user) - - def _user_has_permission(self, user): - return user.is_default_user() - class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): _MODEL_CLS = Intervention diff --git a/konova/views/base.py b/konova/views/base.py index a3ca4716..4961f84d 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -194,7 +194,7 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): if form.is_valid() and geom_form.is_valid(): obj = form.save(request.user, geom_form) - self._REDIRECT_URL = reverse(self._REDIRECT_URL, args=(obj.id,)) + obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,)) generated_identifier = form.cleaned_data.get("identifier", None) @@ -220,7 +220,7 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - return redirect(self._REDIRECT_URL) + return redirect(obj_redirect_url) else: context = self._get_additional_context_data() @@ -234,14 +234,14 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): self._MODEL_CLS, id=id ) - self._REDIRECT_URL = reverse(self._REDIRECT_URL, args=(obj.id,)) + obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,)) if obj.is_recorded: messages.info( request, RECORDED_BLOCKS_EDIT ) - return redirect(self._REDIRECT_URL) + return redirect(obj_redirect_url) form: BaseForm = self._FORM_CLS(None, instance=obj, user=request.user) geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, instance=obj, read_only=False) @@ -262,15 +262,13 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): self._MODEL_CLS, id=id ) - self._REDIRECT_URL = reverse(self._REDIRECT_URL, args=(obj.id,)) + obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,)) form: BaseForm = self._FORM_CLS(request.POST or None, instance=obj, user=request.user) - geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, instance=obj, read_only=False) + geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(request.POST or None, instance=obj, read_only=False) if form.is_valid() and geom_form.is_valid(): obj = form.save(request.user, geom_form) - self._REDIRECT_URL = reverse(self._REDIRECT_URL, args=(obj.id,)) - messages.success(request, _("{} edited").format(obj.identifier)) # The data form takes the geom form for processing, as well as the performing user @@ -292,7 +290,7 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - return redirect(self._REDIRECT_URL) + return redirect(obj_redirect_url) else: context = self._get_additional_context_data() @@ -305,3 +303,10 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): } ) return render(request, self._TEMPLATE, context) + + def _user_has_shared_access(self, user, **kwargs): + obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get('id', None)) + return obj.is_shared_with(user) + + def _user_has_permission(self, user): + return user.is_default_user() \ No newline at end of file diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 4f5c1c6cdbcd85104ef8449094f69ddd59f917aa..d35d602883beb5424bdfc1c4205c1aa7daa5e18a 100644 GIT binary patch delta 11372 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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:111 msgid "New compensation" msgstr "Neue Kompensation" @@ -475,7 +475,7 @@ 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/views/eco_account/eco_account.py:93 msgid "New Eco-Account" msgstr "Neues Ökokonto" @@ -1288,44 +1288,39 @@ msgstr "" msgid "Responsible data" msgstr "Daten zu den verantwortlichen Stellen" -#: compensation/views/compensation/compensation.py:58 +#: compensation/views/compensation/compensation.py:34 msgid "Compensations - Overview" msgstr "Kompensationen - Übersicht" -#: compensation/views/compensation/compensation.py:181 +#: compensation/views/compensation/compensation.py:158 #: 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:181 +#: compensation/views/eco_account/eco_account.py:159 ema/views/ema.py:213 +#: intervention/views/intervention.py:59 intervention/views/intervention.py:186 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 -msgid "Report {}" -msgstr "Bericht {}" - -#: compensation/views/eco_account/eco_account.py:53 +#: compensation/views/eco_account/eco_account.py:32 msgid "Eco-account - Overview" msgstr "Ökokonten - Übersicht" -#: compensation/views/eco_account/eco_account.py:86 +#: compensation/views/eco_account/eco_account.py:70 msgid "Eco-Account {} added" msgstr "Ökokonto {} hinzugefügt" -#: compensation/views/eco_account/eco_account.py:158 +#: compensation/views/eco_account/eco_account.py:136 msgid "Eco-Account {} edited" msgstr "Ökokonto {} bearbeitet" -#: compensation/views/eco_account/eco_account.py:288 +#: compensation/views/eco_account/eco_account.py:260 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:92 +#: ema/views/ema.py:102 msgid "New EMA" msgstr "Neue EMA hinzufügen" @@ -1353,19 +1348,19 @@ msgstr "" msgid "Payment funded compensation" msgstr "Ersatzzahlungsmaßnahme" -#: ema/views/ema.py:53 +#: ema/views/ema.py:31 msgid "EMAs - Overview" msgstr "EMAs - Übersicht" -#: ema/views/ema.py:86 +#: ema/views/ema.py:70 msgid "EMA {} added" msgstr "EMA {} hinzugefügt" -#: ema/views/ema.py:223 +#: ema/views/ema.py:190 msgid "EMA {} edited" msgstr "EMA {} bearbeitet" -#: ema/views/ema.py:262 +#: ema/views/ema.py:237 msgid "EMA removed" msgstr "EMA entfernt" @@ -1429,7 +1424,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:51 msgid "New intervention" msgstr "Neuer Eingriff" @@ -1665,19 +1660,15 @@ msgstr "" msgid "Check performed" msgstr "Prüfung durchgeführt" -#: intervention/views/intervention.py:57 +#: intervention/views/intervention.py:33 msgid "Interventions - Overview" msgstr "Eingriffe - Übersicht" -#: intervention/views/intervention.py:90 -msgid "Intervention {} added" -msgstr "Eingriff {} hinzugefügt" - -#: intervention/views/intervention.py:236 +#: intervention/views/intervention.py:161 msgid "Intervention {} edited" msgstr "Eingriff {} bearbeitet" -#: intervention/views/intervention.py:278 +#: intervention/views/intervention.py:211 msgid "{} removed" msgstr "{} entfernt" @@ -1689,7 +1680,7 @@ msgstr "Hierfür müssen Sie Mitarbeiter sein!" msgid "You need to be administrator to perform this action!" msgstr "Hierfür müssen Sie Administrator sein!" -#: konova/decorators.py:65 +#: konova/decorators.py:65 konova/utils/general.py:40 msgid "" "+++ Attention: You are not part of any group. You won't be able to create, " "edit or do anything. Please contact an administrator. +++" @@ -1801,7 +1792,7 @@ msgstr "Sucht nach Einträgen, an denen diese Person gearbeitet hat" msgid "Save" msgstr "Speichern" -#: konova/forms/base_form.py:72 +#: konova/forms/base_form.py:74 msgid "Not editable" msgstr "Nicht editierbar" @@ -1810,7 +1801,7 @@ msgstr "Nicht editierbar" msgid "Geometry" msgstr "Geometrie" -#: konova/forms/geometry_form.py:100 +#: konova/forms/geometry_form.py:101 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 +2259,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" @@ -2310,7 +2302,15 @@ msgstr "" "Dieses Datum ist unrealistisch. Geben Sie bitte das korrekte Datum ein " "(>1950)." -#: konova/views/home.py:75 templates/navbars/navbar.html:16 +#: konova/views/base.py:209 +msgid "{} added" +msgstr "{} hinzugefügt" + +#: konova/views/base.py:274 +msgid "{} edited" +msgstr "{} bearbeitet" + +#: konova/views/home.py:72 templates/navbars/navbar.html:16 msgid "Home" msgstr "Home" @@ -2330,6 +2330,10 @@ msgstr "{} verzeichnet" msgid "Errors found:" msgstr "Fehler gefunden:" +#: konova/views/report.py:21 +msgid "Report {}" +msgstr "Bericht {}" + #: konova/views/resubmission.py:39 msgid "Resubmission set" msgstr "Wiedervorlage gesetzt" @@ -3056,7 +3060,7 @@ msgid "Manage teams" msgstr "" #: user/templates/user/index.html:53 user/templates/user/team/index.html:19 -#: user/views/views.py:135 +#: user/views/views.py:134 msgid "Teams" msgstr "" @@ -3116,34 +3120,34 @@ msgstr "Läuft ab am" msgid "User API token" msgstr "API Nutzer Token" -#: user/views/views.py:33 +#: user/views/views.py:31 msgid "User settings" msgstr "Einstellungen" -#: user/views/views.py:59 -msgid "Notifications edited" -msgstr "Benachrichtigungen bearbeitet" - -#: user/views/views.py:71 +#: user/views/views.py:44 msgid "User notifications" msgstr "Benachrichtigungen" -#: user/views/views.py:147 +#: user/views/views.py:64 +msgid "Notifications edited" +msgstr "Benachrichtigungen bearbeitet" + +#: user/views/views.py:152 msgid "New team added" msgstr "Neues Team hinzugefügt" -#: user/views/views.py:162 +#: user/views/views.py:167 msgid "Team edited" msgstr "Team bearbeitet" -#: user/views/views.py:177 +#: user/views/views.py:182 msgid "Team removed" msgstr "Team gelöscht" -#: user/views/views.py:192 +#: user/views/views.py:197 msgid "You are not a member of this team" msgstr "Sie sind kein Mitglied dieses Teams" -#: user/views/views.py:199 +#: user/views/views.py:204 msgid "Left Team" msgstr "Team verlassen" -- 2.47.2 From 73178b3fd2dbdbb14631de293ebe8c2ee2389138 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Sun, 19 Oct 2025 14:06:14 +0200 Subject: [PATCH 17/36] # NewCompensation EditCompensation view * refactors new and edit compensation views from function to class based * adds checked property to compensation to return parent-intervention's checked info * fixes bug where compensation could be added to recorded intervention * updates translations --- compensation/forms/compensation.py | 11 ++ compensation/models/compensation.py | 6 + compensation/urls/compensation.py | 11 +- .../views/compensation/compensation.py | 187 +++++------------- konova/views/base.py | 11 +- locale/de/LC_MESSAGES/django.mo | Bin 46205 -> 46354 bytes locale/de/LC_MESSAGES/django.po | 58 +++--- 7 files changed, 118 insertions(+), 166 deletions(-) diff --git a/compensation/forms/compensation.py b/compensation/forms/compensation.py index bb1532d6..b3ceb03f 100644 --- a/compensation/forms/compensation.py +++ b/compensation/forms/compensation.py @@ -168,6 +168,17 @@ class NewCompensationForm(AbstractCompensationForm, comp.log.add(action) return comp, action + def is_valid(self): + valid = super().is_valid() + intervention = self.cleaned_data.get("intervention", None) + valid &= not intervention.is_recorded + if not valid: + self.add_error( + "intervention", + _("This intervention is currently recorded. You cannot add further compensations as long as it is recorded.") + ) + return valid + def save(self, user: User, geom_form: SimpleGeomForm): with transaction.atomic(): comp, action = self.__create_comp(user) diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index 49accd46..eebd2b38 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -510,6 +510,12 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin): return retval + @property + def checked(self): + if self.intervention: + return self.intervention.checked + return None + class CompensationDocument(AbstractDocument): """ diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index adec4b7f..b410ac78 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -17,19 +17,20 @@ 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, \ - remove_view, CompensationIndexView, CompensationIdentifierGeneratorView, CompensationDetailView +from compensation.views.compensation.compensation import \ + remove_view, CompensationIndexView, CompensationIdentifierGeneratorView, CompensationDetailView, \ + NewCompensationFormView, EditCompensationFormView from compensation.views.compensation.log import CompensationLogView urlpatterns = [ # Main compensation path("", CompensationIndexView.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/', NewCompensationFormView.as_view(), name='new'), + path('new', NewCompensationFormView.as_view(), name='new'), path('', CompensationDetailView.as_view(), name='detail'), path('/log', CompensationLogView.as_view(), name='log'), - path('/edit', edit_view, name='edit'), + path('/edit', EditCompensationFormView.as_view(), name='edit'), path('/remove', remove_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 2403d2b8..c9baedb0 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -18,15 +18,12 @@ from compensation.forms.compensation import EditCompensationForm, NewCompensatio 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.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, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ - RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \ - COMPENSATION_ADDED_TEMPLATE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE -from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView + RECORDED_BLOCKS_EDIT, PARAMS_INVALID +from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ + BaseEditSpatialLocatedObjectFormView from konova.views.detail import BaseDetailView @@ -44,74 +41,55 @@ class CompensationIndexView(LoginRequiredMixin, BaseIndexView): return qs -@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 +class NewCompensationFormView(BaseNewSpatialLocatedObjectFormView): + _FORM_CLS = NewCompensationForm + _MODEL_CLS = Compensation + _TEMPLATE = "compensation/form/view.html" + _TAB_TITLE = _("New Compensation") + _REDIRECT_URL = "compensation:detail" - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "compensation/form/view.html" - if intervention_id is not None: - try: - intervention = Intervention.objects.get(id=intervention_id) - except ObjectDoesNotExist: - messages.error(request, PARAMS_INVALID) - return redirect("home") - if intervention.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("intervention:detail", id=intervention_id) - - data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id) - geom_form = SimpleGeomForm(request.POST or None, read_only=False) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - generated_identifier = data_form.cleaned_data.get("identifier", None) - comp = data_form.save(request.user, geom_form) - if generated_identifier != comp.identifier: - messages.info( - request, - IDENTIFIER_REPLACED.format( - generated_identifier, - comp.identifier - ) - ) - messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier)) - if geom_form.has_geometry_simplified(): - messages.info( - 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) + def _user_has_shared_access(self, user, **kwargs): + # On a new compensation make sure the intervention (if call came directly through an intervention's detail + # view) is shared with the user + intervention_id = kwargs.get("intervention_id", None) + if not intervention_id: + return True 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) + intervention = get_object_or_404(Intervention, id=intervention_id) + return intervention.is_shared_with(user) + + def _user_has_permission(self, user): + # User has to be an ets user + return user.is_default_user() + + def dispatch(self, request, *args, **kwargs): + # Make sure there is an existing intervention based on the given id + # Compensations can not exist without an intervention + intervention_id = kwargs.get("intervention_id", None) + if intervention_id: + try: + intervention = Intervention.objects.get(id=intervention_id) + if intervention.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("intervention:detail", id=intervention_id) + except ObjectDoesNotExist: + messages.error(request, PARAMS_INVALID, extra_tags="danger") + return redirect("home") + return super().dispatch(request, *args, **kwargs) + + +class EditCompensationFormView(BaseEditSpatialLocatedObjectFormView): + _MODEL_CLS = Compensation + _FORM_CLS = EditCompensationForm + _TEMPLATE = "compensation/form/view.html" + _REDIRECT_URL = "compensation:detail" + + def _user_has_permission(self, user): + # User has to be an ets user + return user.is_default_user() class CompensationIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): @@ -119,71 +97,6 @@ class CompensationIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGene _REDIRECT_URL = "compensation:index" -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing compensations - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "compensation/form/view.html" - # Get object from db - comp = get_object_or_404(Compensation, id=id) - if comp.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("compensation:detail", id=id) - - # Create forms, initialize with values from db/from POST request - data_form = EditCompensationForm(request.POST or None, instance=comp) - geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - # Preserve state of intervention 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: - messages.info(request, CHECK_STATE_RESET) - messages.success(request, _("Compensation {} edited").format(comp.identifier)) - if geom_form.has_geometry_simplified(): - messages.info( - 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) - - class CompensationDetailView(BaseDetailView): _MODEL_CLS = Compensation _TEMPLATE = "compensation/detail/compensation/view.html" diff --git a/konova/views/base.py b/konova/views/base.py index 4961f84d..266ddee0 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -225,10 +225,19 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): context = self._get_additional_context_data() context = BaseContext(request, additional_context=context).context + context.update( + { + "form": form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + ) return render(request, self._TEMPLATE, context) class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): + _TAB_TITLE = _("Edit {}") + def get(self, request: HttpRequest, id: str): obj = get_object_or_404( self._MODEL_CLS, @@ -252,7 +261,7 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): { "form": form, "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE.format(obj.identifier), } ) return render(request, self._TEMPLATE, context) diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index d35d602883beb5424bdfc1c4205c1aa7daa5e18a..c9a22673b615d664eebe6e2487fcf12849e6deaf 100644 GIT binary patch delta 11811 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\n" "Language-Team: LANGUAGE \n" @@ -448,11 +448,19 @@ 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:111 +#: compensation/views/compensation/compensation.py:161 msgid "New compensation" msgstr "Neue Kompensation" -#: compensation/forms/compensation.py:190 +#: compensation/forms/compensation.py:179 +msgid "" +"This intervention is currently recorded. You cannot add further " +"compensations as long as it is recorded." +msgstr "" +"Dieser Eingriff ist derzeit verzeichnet. " +"Sie können keine weiteren Kompensationen hinzufügen, so lange er verzeichnet ist." + +#: compensation/forms/compensation.py:202 msgid "Edit compensation" msgstr "Bearbeite Kompensation" @@ -1288,18 +1296,25 @@ msgstr "" msgid "Responsible data" msgstr "Daten zu den verantwortlichen Stellen" -#: compensation/views/compensation/compensation.py:34 +#: compensation/views/compensation/compensation.py:35 msgid "Compensations - Overview" msgstr "Kompensationen - Übersicht" -#: compensation/views/compensation/compensation.py:158 +#: compensation/views/compensation/compensation.py:52 +#, fuzzy +#| msgid "New compensation" +msgid "New Compensation" +msgstr "Neue Kompensation" + +#: compensation/views/compensation/compensation.py:208 #: konova/utils/message_templates.py:40 msgid "Compensation {} edited" msgstr "Kompensation {} bearbeitet" -#: compensation/views/compensation/compensation.py:181 -#: compensation/views/eco_account/eco_account.py:159 ema/views/ema.py:213 -#: intervention/views/intervention.py:59 intervention/views/intervention.py:186 +#: compensation/views/compensation/compensation.py:231 +#: compensation/views/eco_account/eco_account.py:159 ema/views/ema.py:59 +#: intervention/views/intervention.py:59 intervention/views/intervention.py:179 +#: konova/views/base.py:239 msgid "Edit {}" msgstr "Bearbeite {}" @@ -1319,8 +1334,7 @@ msgstr "Ökokonto {} bearbeitet" msgid "Eco-account removed" msgstr "Ökokonto entfernt" -#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:92 -#: ema/views/ema.py:102 +#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:42 msgid "New EMA" msgstr "Neue EMA hinzufügen" @@ -1348,19 +1362,11 @@ msgstr "" msgid "Payment funded compensation" msgstr "Ersatzzahlungsmaßnahme" -#: ema/views/ema.py:31 +#: ema/views/ema.py:26 msgid "EMAs - Overview" msgstr "EMAs - Übersicht" -#: ema/views/ema.py:70 -msgid "EMA {} added" -msgstr "EMA {} hinzugefügt" - -#: ema/views/ema.py:190 -msgid "EMA {} edited" -msgstr "EMA {} bearbeitet" - -#: ema/views/ema.py:237 +#: ema/views/ema.py:138 msgid "EMA removed" msgstr "EMA entfernt" @@ -1664,11 +1670,11 @@ msgstr "Prüfung durchgeführt" msgid "Interventions - Overview" msgstr "Eingriffe - Übersicht" -#: intervention/views/intervention.py:161 +#: intervention/views/intervention.py:154 msgid "Intervention {} edited" msgstr "Eingriff {} bearbeitet" -#: intervention/views/intervention.py:211 +#: intervention/views/intervention.py:204 msgid "{} removed" msgstr "{} entfernt" @@ -2306,7 +2312,7 @@ msgstr "" msgid "{} added" msgstr "{} hinzugefügt" -#: konova/views/base.py:274 +#: konova/views/base.py:281 msgid "{} edited" msgstr "{} bearbeitet" @@ -3151,3 +3157,9 @@ msgstr "Sie sind kein Mitglied dieses Teams" #: user/views/views.py:204 msgid "Left Team" msgstr "Team verlassen" + +#~ msgid "EMA {} added" +#~ msgstr "EMA {} hinzugefügt" + +#~ msgid "EMA {} edited" +#~ msgstr "EMA {} bearbeitet" -- 2.47.2 From a86d86b73195e50c00949dfa8d302842788599f6 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 20 Oct 2025 09:49:18 +0200 Subject: [PATCH 18/36] # NewEcoAccount EditEcoAccount view * refactors new and edit eco account views from function to class based * removes info message if checked intervention is altered and loses the current checked state * updates comments/documentation * removes code duplicates * fixes display error where modal form was hidden behind menu bar of map client * fixes bug where compensation could not be created directly from intervention --- compensation/models/compensation.py | 6 ---- compensation/urls/eco_account.py | 9 ++--- .../views/compensation/compensation.py | 2 +- compensation/views/eco_account/eco_account.py | 26 +++++++++++++- ema/views/ema.py | 4 --- konova/forms/base_form.py | 2 +- konova/static/css/konova.css | 4 +++ konova/views/base.py | 36 ++++++++++--------- 8 files changed, 56 insertions(+), 33 deletions(-) diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index eebd2b38..49accd46 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -510,12 +510,6 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin): return retval - @property - def checked(self): - if self.intervention: - return self.intervention.checked - return None - class CompensationDocument(AbstractDocument): """ diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index f4989be7..fc3c6116 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.eco_account import new_view, edit_view, remove_view, \ - EcoAccountIndexView, EcoAccountIdentifierGeneratorView, EcoAccountDetailView +from compensation.views.eco_account.eco_account import remove_view, \ + EcoAccountIndexView, EcoAccountIdentifierGeneratorView, EcoAccountDetailView, NewEcoAccountFormView, \ + EditEcoAccountFormView from compensation.views.eco_account.log import EcoAccountLogView from compensation.views.eco_account.record import EcoAccountRecordView from compensation.views.eco_account.report import EcoAccountReportView @@ -29,13 +30,13 @@ from compensation.views.eco_account.deduction import NewEcoAccountDeductionView, app_name = "acc" urlpatterns = [ path("", EcoAccountIndexView.as_view(), name="index"), - path('new/', new_view, name='new'), + path('new/', NewEcoAccountFormView.as_view(), name='new'), path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'), path('', EcoAccountDetailView.as_view(), name='detail'), path('/log', EcoAccountLogView.as_view(), name='log'), path('/record', EcoAccountRecordView.as_view(), name='record'), path('/report', EcoAccountReportView.as_view(), name='report'), - path('/edit', edit_view, name='edit'), + path('/edit', EditEcoAccountFormView.as_view(), name='edit'), path('/remove', remove_view, name='remove'), path('/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'), diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index c9baedb0..35ff6510 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -88,7 +88,7 @@ class EditCompensationFormView(BaseEditSpatialLocatedObjectFormView): _REDIRECT_URL = "compensation:detail" def _user_has_permission(self, user): - # User has to be an ets user + # User has to be a default user return user.is_default_user() diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 42eb569e..95dd8005 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -23,7 +23,8 @@ 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, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE -from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView +from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ + BaseEditSpatialLocatedObjectFormView from konova.views.detail import BaseDetailView @@ -40,6 +41,29 @@ class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView): return qs +class NewEcoAccountFormView(BaseNewSpatialLocatedObjectFormView): + _FORM_CLS = NewEcoAccountForm + _MODEL_CLS = EcoAccount + _TEMPLATE = "compensation/form/view.html" + _TAB_TITLE = _("New Eco-Account") + _REDIRECT_URL = "compensation:acc:detail" + + def _user_has_permission(self, user): + # User has to be a default user + return user.is_default_user() + + +class EditEcoAccountFormView(BaseEditSpatialLocatedObjectFormView): + _FORM_CLS = EditEcoAccountForm + _MODEL_CLS = EcoAccount + _TEMPLATE = "compensation/form/view.html" + _REDIRECT_URL = "compensation:acc:detail" + + def _user_has_permission(self, user): + # User has to be a default user + return user.is_default_user() + + @login_required @default_group_required def new_view(request: HttpRequest): diff --git a/ema/views/ema.py b/ema/views/ema.py index 4a287ab9..d454ffba 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -46,10 +46,6 @@ class NewEmaFormView(BaseNewSpatialLocatedObjectFormView): # User has to be an ets user return user.is_ets_user() - def _user_has_shared_access(self, user, **kwargs): - # No specific share constraint for creatin EMA entries - return True - class EditEmaFormView(BaseEditSpatialLocatedObjectFormView): _MODEL_CLS = Ema diff --git a/konova/forms/base_form.py b/konova/forms/base_form.py index 337f1a75..ba526396 100644 --- a/konova/forms/base_form.py +++ b/konova/forms/base_form.py @@ -48,7 +48,7 @@ class BaseForm(forms.Form): self.__check_valid_label_input_ratio() @abstractmethod - def save(self): + def save(self, *arg, **kwargs): # To be implemented in subclasses! pass diff --git a/konova/static/css/konova.css b/konova/static/css/konova.css index 0c872967..1067d230 100644 --- a/konova/static/css/konova.css +++ b/konova/static/css/konova.css @@ -288,4 +288,8 @@ Overwrites netgis.css attributes Overwrites gradient used on default css of netgis map client */ background: var(--rlp-red) !important; +} + +.modal{ + z-index: 100000; } \ No newline at end of file diff --git a/konova/views/base.py b/konova/views/base.py index 266ddee0..eadcc524 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -18,7 +18,7 @@ from konova.forms import BaseForm, SimpleGeomForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.general import check_user_is_in_any_group from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED, IDENTIFIER_REPLACED, \ - GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET + GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID class BaseView(View): @@ -152,7 +152,15 @@ class BaseFormView(BaseView): class Meta: abstract = True - def _get_additional_context_data(self, **kwargs): + def _get_additional_context(self, **kwargs): + """ + + Args: + **kwargs (): + + Returns: + + """ return {} @@ -173,11 +181,11 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): # There is no shared access control since nothing exists yet return True - def get(self, request: HttpRequest): - form: BaseForm = self._FORM_CLS(None, user=request.user) + def get(self, request: HttpRequest, **kwargs): + form: BaseForm = self._FORM_CLS(None, **kwargs, user=request.user) geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, user=request.user, read_only=False) - context = self._get_additional_context_data() + context = self._get_additional_context() context = BaseContext(request, additional_context=context).context context.update( { @@ -188,8 +196,8 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): ) return render(request, self._TEMPLATE, context) - def post(self, request: HttpRequest): - form: BaseForm = self._FORM_CLS(request.POST or None, user=request.user) + def post(self, request: HttpRequest, **kwargs): + form: BaseForm = self._FORM_CLS(request.POST or None, **kwargs, user=request.user) geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(request.POST or None, user=request.user, read_only=False) if form.is_valid() and geom_form.is_valid(): @@ -222,7 +230,8 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): return redirect(obj_redirect_url) else: - context = self._get_additional_context_data() + context = self._get_additional_context() + messages.error(request, FORM_INVALID, extra_tags="danger",) context = BaseContext(request, additional_context=context).context context.update( @@ -255,7 +264,7 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): form: BaseForm = self._FORM_CLS(None, instance=obj, user=request.user) geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, instance=obj, read_only=False) - context = self._get_additional_context_data() + context = self._get_additional_context() context = BaseContext(request, additional_context=context).context context.update( { @@ -280,12 +289,6 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): obj = form.save(request.user, geom_form) messages.success(request, _("{} edited").format(obj.identifier)) - # 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 - obj_is_checked = obj.checked is not None - if obj_is_checked: - messages.info(request, CHECK_STATE_RESET) - if geom_form.has_geometry_simplified(): messages.info( request, @@ -301,7 +304,8 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): return redirect(obj_redirect_url) else: - context = self._get_additional_context_data() + context = self._get_additional_context() + messages.error(request, FORM_INVALID, extra_tags="danger",) context = BaseContext(request, additional_context=context).context context.update( -- 2.47.2 From ed5d5717041c78a40b34f8f9951851bf4b235515 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 20 Oct 2025 13:52:15 +0200 Subject: [PATCH 19/36] # Bugfix NewCompensationForm * fixes bug where a form error would trigger a wrong error warning --- compensation/forms/compensation.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/compensation/forms/compensation.py b/compensation/forms/compensation.py index b3ceb03f..51392a2f 100644 --- a/compensation/forms/compensation.py +++ b/compensation/forms/compensation.py @@ -171,8 +171,8 @@ class NewCompensationForm(AbstractCompensationForm, def is_valid(self): valid = super().is_valid() intervention = self.cleaned_data.get("intervention", None) - valid &= not intervention.is_recorded - if not valid: + if intervention.is_recorded: + valid &= False self.add_error( "intervention", _("This intervention is currently recorded. You cannot add further compensations as long as it is recorded.") -- 2.47.2 From 97fbe0274210d644386c4ecf2d2acebbfecd04f3 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 20 Oct 2025 16:13:58 +0200 Subject: [PATCH 20/36] # BaseModalFormView refactoring * extends BaseModalFormView to hold general logic for processing GET and POST requests for BaseModalForm endpoints * refactors uuid check to use a specific parameter instead of kwargs * fixes css bug where modal form input elements would not be visible * refactors check view for intervention from function to class * refactors DeductionViews to inherit from extended BaseModalFormView --- compensation/views/eco_account/deduction.py | 13 +-- intervention/forms/modals/deduction.py | 18 ++-- intervention/urls.py | 4 +- intervention/views/check.py | 37 +++---- intervention/views/deduction.py | 17 +-- konova/static/css/konova.css | 4 +- konova/utils/general.py | 3 +- konova/views/base.py | 57 +++++++++- konova/views/deduction.py | 110 +++++--------------- konova/views/detail.py | 2 +- 10 files changed, 127 insertions(+), 138 deletions(-) diff --git a/compensation/views/eco_account/deduction.py b/compensation/views/eco_account/deduction.py index 80040a20..01e84f3f 100644 --- a/compensation/views/eco_account/deduction.py +++ b/compensation/views/eco_account/deduction.py @@ -11,10 +11,11 @@ from django.http import Http404 from compensation.models import EcoAccount from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView +_ECO_ACCOUNT_DETAIl_URL_NAME = "compensation:acc:detail" class NewEcoAccountDeductionView(LoginRequiredMixin, AbstractNewDeductionView): - _MODEL = EcoAccount - _REDIRECT_URL = "compensation:acc:detail" + _MODEL_CLS = EcoAccount + _REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME def _custom_check(self, obj): # New deductions can only be created if the eco account has been recorded @@ -23,10 +24,10 @@ class NewEcoAccountDeductionView(LoginRequiredMixin, AbstractNewDeductionView): class EditEcoAccountDeductionView(LoginRequiredMixin, AbstractEditDeductionView): - _MODEL = EcoAccount - _REDIRECT_URL = "compensation:acc:detail" + _MODEL_CLS = EcoAccount + _REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME class RemoveEcoAccountDeductionView(LoginRequiredMixin, AbstractRemoveDeductionView): - _MODEL = EcoAccount - _REDIRECT_URL = "compensation:acc:detail" + _MODEL_CLS = EcoAccount + _REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME diff --git a/intervention/forms/modals/deduction.py b/intervention/forms/modals/deduction.py index 8e12a442..c3fe245d 100644 --- a/intervention/forms/modals/deduction.py +++ b/intervention/forms/modals/deduction.py @@ -172,7 +172,8 @@ class EditEcoAccountDeductionModalForm(NewEcoAccountDeductionModalForm): deduction = None def __init__(self, *args, **kwargs): - self.deduction = kwargs.pop("deduction", None) + deduction_id = kwargs.pop("deduction_id", None) + self.deduction = EcoAccountDeduction.objects.get(id=deduction_id) super().__init__(*args, **kwargs) self.form_title = _("Edit Deduction") form_data = { @@ -252,19 +253,20 @@ class RemoveEcoAccountDeductionModalForm(RemoveModalForm): Can be used for anything, where removing shall be confirmed by the user a second time. """ - deduction = None + _DEDUCTION_OBJ = None def __init__(self, *args, **kwargs): - deduction = kwargs.pop("deduction", None) - self.deduction = deduction + deduction_id = kwargs.pop("deduction_id", None) + deduction = EcoAccountDeduction.objects.get(id=deduction_id) + self._DEDUCTION_OBJ = deduction super().__init__(*args, **kwargs) def save(self): with transaction.atomic(): - self.deduction.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) - self.deduction.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) - self.deduction.delete() + self._DEDUCTION_OBJ.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) + self._DEDUCTION_OBJ.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) + self._DEDUCTION_OBJ.delete() def check_for_recorded_instance(self): - if self.deduction.intervention.is_recorded: + if self._DEDUCTION_OBJ.intervention.is_recorded: self.block_form() diff --git a/intervention/urls.py b/intervention/urls.py index dae2d47d..e179d000 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 @@ -36,7 +36,7 @@ urlpatterns = [ path('/remove', remove_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', InterventionReportView.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..3ed0cdfb 100644 --- a/intervention/views/check.py +++ b/intervention/views/check.py @@ -5,35 +5,26 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 +from django.contrib.auth.mixins import LoginRequiredMixin from django.utils.translation import gettext_lazy as _ from intervention.forms.modals.check import CheckModalForm from intervention.models import Intervention -from konova.decorators import registration_office_group_required, shared_access_required from konova.utils.message_templates import INTERVENTION_INVALID +from konova.views.base import BaseModalFormView -@login_required -@registration_office_group_required -@shared_access_required(Intervention, "id") -def check_view(request: HttpRequest, id: str): - """ Renders check form for an intervention +class InterventionCheckView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = Intervention + _FORM_CLS = CheckModalForm + _MSG_SUCCESS = _("Check performed") + _MSG_ERROR = INTERVENTION_INVALID + _REDIRECT_URL = "intervention:detail" - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - form = CheckModalForm(request.POST or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=_("Check performed"), - msg_error=INTERVENTION_INVALID - ) + def _user_has_permission(self, user): + return user.is_zb_user() + def _get_redirect_url(self, *args, **kwargs): + redirect_url = super()._get_redirect_url(*args, **kwargs) + redirect_url += "#related_data" + return redirect_url diff --git a/intervention/views/deduction.py b/intervention/views/deduction.py index 122c1dba..8aed81bf 100644 --- a/intervention/views/deduction.py +++ b/intervention/views/deduction.py @@ -8,19 +8,24 @@ Created on: 19.08.22 from django.contrib.auth.mixins import LoginRequiredMixin from intervention.models import Intervention +from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_EDITED, DEDUCTION_REMOVED from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView +_INTERVENTION_DETAIL_URL_NAME = "intervention:detail" class NewInterventionDeductionView(LoginRequiredMixin, AbstractNewDeductionView): - _MODEL = Intervention - _REDIRECT_URL = "intervention:detail" + _MODEL_CLS = Intervention + _MSG_SUCCESS = DEDUCTION_ADDED + _REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME class EditInterventionDeductionView(LoginRequiredMixin, AbstractEditDeductionView): - _MODEL = Intervention - _REDIRECT_URL = "intervention:detail" + _MODEL_CLS = Intervention + _MSG_SUCCESS = DEDUCTION_EDITED + _REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME class RemoveInterventionDeductionView(LoginRequiredMixin, AbstractRemoveDeductionView): - _MODEL = Intervention - _REDIRECT_URL = "intervention:detail" + _MODEL_CLS = Intervention + _MSG_SUCCESS = DEDUCTION_REMOVED + _REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME diff --git a/konova/static/css/konova.css b/konova/static/css/konova.css index 1067d230..8eb5f447 100644 --- a/konova/static/css/konova.css +++ b/konova/static/css/konova.css @@ -290,6 +290,6 @@ Overwrites netgis.css attributes background: var(--rlp-red) !important; } -.modal{ - z-index: 100000; +.netgis-menu{ + z-index: 100 !important; } \ No newline at end of file diff --git a/konova/utils/general.py b/konova/utils/general.py index 666d4811..dd7dc1ea 100644 --- a/konova/utils/general.py +++ b/konova/utils/general.py @@ -41,8 +41,7 @@ def check_user_is_in_any_group(request: HttpRequest): ) return request -def check_id_is_valid_uuid(**kwargs: dict): - uuid = kwargs.get("uuid", None) or kwargs.get("id", None) +def check_id_is_valid_uuid(uuid: str): if uuid: try: # Check whether the id is a proper uuid or something that would break a db fetch diff --git a/konova/views/base.py b/konova/views/base.py index eadcc524..5cc4fefe 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -5,9 +5,10 @@ Created on: 15.10.25 """ from abc import abstractmethod +from bootstrap_modal_forms.mixins import is_ajax from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import HttpRequest, JsonResponse +from django.http import HttpRequest, JsonResponse, HttpResponseRedirect from django.shortcuts import render, redirect, get_object_or_404 from django.urls import reverse from django.views import View @@ -16,9 +17,9 @@ from django.utils.translation import gettext_lazy as _ from konova.contexts import BaseContext from konova.forms import BaseForm, SimpleGeomForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.general import check_user_is_in_any_group +from konova.utils.general import check_user_is_in_any_group, check_id_is_valid_uuid from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED, IDENTIFIER_REPLACED, \ - GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID + GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, RECORDED_BLOCKS_EDIT, FORM_INVALID class BaseView(View): @@ -65,14 +66,64 @@ class BaseView(View): """ return False + def _get_redirect_url(self, *args, **kwargs): + return self._REDIRECT_URL + + def _get_redirect_url_error(self, *args, **kwargs): + return self._REDIRECT_URL_ERROR class BaseModalFormView(BaseView): _TEMPLATE = "modal/modal_form.html" + _MODEL_CLS = None + _FORM_CLS = None _TAB_TITLE = None + _MSG_SUCCESS = None + _MSG_ERROR = None class Meta: abstract = True + def _user_has_shared_access(self, user, **kwargs): + obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get("id")) + return obj.is_shared_with(user) + + def get(self, request: HttpRequest, id: str, *args, **kwargs): + obj = self._MODEL_CLS.objects.get(id=id) + form = self._FORM_CLS(request.POST or None, instance=obj, request=request, **kwargs) + context = { + "form": form, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + def post(self, request: HttpRequest, id: str, *args, **kwargs): + obj = self._MODEL_CLS.objects.get(id=id) + form = self._FORM_CLS(request.POST or None, instance=obj, request=request, **kwargs) + redirect_url = self._get_redirect_url(obj=obj) + if form.is_valid(): + if not is_ajax(request.META): + # Modal forms send one POST for checking on data validity. This can be used to return possible errors + # on the form. A second POST (if no errors occured) is sent afterwards and needs to process the + # saving/commiting of the data to the database. is_ajax() performs this check. The first request is + # an ajax call, the second is a regular form POST. + form.save() + messages.success( + request, + self._MSG_SUCCESS + ) + return HttpResponseRedirect(redirect_url) + else: + context = { + "form": form, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + def _get_redirect_url(self, *args, **kwargs): + obj = kwargs.get("obj", None) + assert obj is not None + return reverse(self._REDIRECT_URL, args=(obj.id,)) + class BaseIndexView(BaseView): """ Base class for index views diff --git a/konova/views/deduction.py b/konova/views/deduction.py index 539ab108..b7b9968c 100644 --- a/konova/views/deduction.py +++ b/konova/views/deduction.py @@ -6,20 +6,21 @@ Created on: 22.08.22 """ from django.core.exceptions import ObjectDoesNotExist -from django.http import Http404, HttpRequest -from django.shortcuts import get_object_or_404 from django.urls import reverse from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \ RemoveEcoAccountDeductionModalForm -from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_EDITED, DEDUCTION_REMOVED, DEDUCTION_UNKNOWN +from konova.utils.general import check_id_is_valid_uuid from konova.views.base import BaseModalFormView class AbstractDeductionView(BaseModalFormView): - _MODEL = None _REDIRECT_URL = None + def dispatch(self, request, *args, **kwargs): + check_id_is_valid_uuid(kwargs.get("id")) + return super().dispatch(request, *args, **kwargs) + def _custom_check(self, obj): """ Can be used by inheriting classes to provide custom checks before further processing @@ -50,7 +51,7 @@ class AbstractDeductionView(BaseModalFormView): """ ret_val: bool = False try: - obj = self._MODEL.objects.get( + obj = self._MODEL_CLS.objects.get( id=kwargs.get("id") ) ret_val = obj.is_shared_with(user) @@ -58,96 +59,35 @@ class AbstractDeductionView(BaseModalFormView): ret_val = False return ret_val + def _get_redirect_url(self, *args, **kwargs): + obj = kwargs.get("obj", None) + assert obj is not None + return reverse(self._REDIRECT_URL, args=(obj.id,)) + "#related_data" + class AbstractNewDeductionView(AbstractDeductionView): + _FORM_CLS = NewEcoAccountDeductionModalForm + class Meta: abstract = True - def get(self, request, id: str): - """ Renders a modal form view for creating deductions - - Args: - request (HttpRequest): The incoming request - id (str): The obj's id which shall benefit from this deduction - - Returns: - - """ - obj = get_object_or_404(self._MODEL, id=id) - self._custom_check(obj) - form = NewEcoAccountDeductionModalForm(request.POST or None, instance=obj, request=request) - return form.process_request( - request, - msg_success=DEDUCTION_ADDED, - redirect_url=reverse(self._REDIRECT_URL, args=(id,)) + "#related_data", - ) - - def post(self, request, id: str): - return self.get(request, id) - class AbstractEditDeductionView(AbstractDeductionView): + _FORM_CLS = EditEcoAccountDeductionModalForm + + def dispatch(self, request, *args, **kwargs): + check_id_is_valid_uuid(kwargs.get("deduction_id")) + return super().dispatch(request, *args, **kwargs) + class Meta: abstract = True - def get(self, request, id: str, deduction_id: str): - """ Renders a modal view for editing deductions - - Args: - request (HttpRequest): The incoming request - id (str): The object's id - deduction_id (str): The deduction's id - - Returns: - - """ - obj = get_object_or_404(self._MODEL, id=id) - self._custom_check(obj) - try: - eco_deduction = obj.deductions.get(id=deduction_id) - except ObjectDoesNotExist: - raise Http404(DEDUCTION_UNKNOWN) - - form = EditEcoAccountDeductionModalForm(request.POST or None, instance=obj, deduction=eco_deduction, - request=request) - return form.process_request( - request=request, - msg_success=DEDUCTION_EDITED, - redirect_url=reverse(self._REDIRECT_URL, args=(id,)) + "#related_data" - ) - - def post(self, request, id: str, deduction_id: str): - return self.get(request, id, deduction_id) - - class AbstractRemoveDeductionView(AbstractDeductionView): + _FORM_CLS = RemoveEcoAccountDeductionModalForm + + def dispatch(self, request, *args, **kwargs): + check_id_is_valid_uuid(kwargs.get("deduction_id")) + return super().dispatch(request, *args, **kwargs) + class Meta: abstract = True - - def get(self, request, id: str, deduction_id: str): - """ Renders a modal view for removing deductions - - Args: - request (HttpRequest): The incoming request - id (str): The object's id - deduction_id (str): The deduction's id - - Returns: - - """ - obj = get_object_or_404(self._MODEL, id=id) - self._custom_check(obj) - try: - eco_deduction = obj.deductions.get(id=deduction_id) - except ObjectDoesNotExist: - raise Http404(DEDUCTION_UNKNOWN) - form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=obj, deduction=eco_deduction, - request=request) - return form.process_request( - request=request, - msg_success=DEDUCTION_REMOVED, - redirect_url=reverse(self._REDIRECT_URL, args=(id,)) + "#related_data" - ) - - def post(self, request, id: str, deduction_id: str): - return self.get(request, id, deduction_id) diff --git a/konova/views/detail.py b/konova/views/detail.py index 6322ab9b..72b4ad17 100644 --- a/konova/views/detail.py +++ b/konova/views/detail.py @@ -26,7 +26,7 @@ class BaseDetailView(LoginRequiredMixin, BaseView): abstract = True def dispatch(self, request, *args, **kwargs): - check_id_is_valid_uuid(**kwargs) + check_id_is_valid_uuid(kwargs.get('id')) return super().dispatch(request, *args, **kwargs) def _user_has_shared_access(self, user, **kwargs): -- 2.47.2 From a7b23935a12af9e5d6f8a6627c7fb98c5892c01c Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 20 Oct 2025 16:29:50 +0200 Subject: [PATCH 21/36] # RecordModalForm refactored * refactors AbstractRecordModalForm * refactors recording view for ema, intervention and eco account --- compensation/views/eco_account/record.py | 16 ++------ ema/views/record.py | 16 ++------ intervention/views/record.py | 15 ++------ konova/views/base.py | 4 +- konova/views/record.py | 49 +++++++----------------- 5 files changed, 29 insertions(+), 71 deletions(-) diff --git a/compensation/views/eco_account/record.py b/compensation/views/eco_account/record.py index 0d1f2070..5394af2f 100644 --- a/compensation/views/eco_account/record.py +++ b/compensation/views/eco_account/record.py @@ -5,20 +5,12 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator +from django.contrib.auth.mixins import LoginRequiredMixin from compensation.models import EcoAccount -from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.views.record import AbstractRecordView -class EcoAccountRecordView(AbstractRecordView): - model = EcoAccount - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(conservation_office_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) +class EcoAccountRecordView(LoginRequiredMixin, AbstractRecordView): + _MODEL_CLS = EcoAccount + _REDIRECT_URL = "compensation:acc:detail" diff --git a/ema/views/record.py b/ema/views/record.py index 83560999..83e7b5a1 100644 --- a/ema/views/record.py +++ b/ema/views/record.py @@ -5,20 +5,12 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator +from django.contrib.auth.mixins import LoginRequiredMixin from ema.models import Ema -from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.views.record import AbstractRecordView -class EmaRecordView(AbstractRecordView): - model = Ema - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(conservation_office_group_required) - @method_decorator(shared_access_required(Ema, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) +class EmaRecordView(LoginRequiredMixin, AbstractRecordView): + _MODEL_CLS = Ema + _REDIRECT_URL = "ema:detail" diff --git a/intervention/views/record.py b/intervention/views/record.py index a845fdfd..d425a89c 100644 --- a/intervention/views/record.py +++ b/intervention/views/record.py @@ -5,19 +5,12 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator +from django.contrib.auth.mixins import LoginRequiredMixin from intervention.models import Intervention -from konova.decorators import conservation_office_group_required, shared_access_required from konova.views.record import AbstractRecordView -class InterventionRecordView(AbstractRecordView): - model = Intervention - - @method_decorator(login_required) - @method_decorator(conservation_office_group_required) - @method_decorator(shared_access_required(Intervention, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) +class InterventionRecordView(LoginRequiredMixin, AbstractRecordView): + _MODEL_CLS = Intervention + _REDIRECT_URL = "intervention:detail" diff --git a/konova/views/base.py b/konova/views/base.py index 5cc4fefe..d8b1ab0b 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -109,7 +109,7 @@ class BaseModalFormView(BaseView): form.save() messages.success( request, - self._MSG_SUCCESS + self._get_msg_success(obj=obj) ) return HttpResponseRedirect(redirect_url) else: @@ -124,6 +124,8 @@ class BaseModalFormView(BaseView): assert obj is not None return reverse(self._REDIRECT_URL, args=(obj.id,)) + def _get_msg_success(self, *args, **kwargs): + return self._MSG_SUCCESS class BaseIndexView(BaseView): """ Base class for index views diff --git a/konova/views/record.py b/konova/views/record.py index b80d1b0f..f2c09640 100644 --- a/konova/views/record.py +++ b/konova/views/record.py @@ -5,46 +5,25 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.shortcuts import get_object_or_404 -from django.views import View from django.utils.translation import gettext_lazy as _ from konova.forms.modals import RecordModalForm +from konova.views.base import BaseModalFormView -class AbstractRecordView(View): - model = None +class AbstractRecordView(BaseModalFormView): + _MODEL_CLS = None + _FORM_CLS = RecordModalForm + _MSG_SUCCESS = None - def get(self, request, id: str): - """ Renders a modal form for recording an object + def _user_has_permission(self, user): + return user.is_ets_user() - Args: - request (HttpRequest): The incoming request - id (str): The object's id + def _get_msg_success(self, *args, **kwargs): + obj = kwargs.get("obj") + assert obj is not None - Returns: - - """ - obj = get_object_or_404(self.model, id=id) - form = RecordModalForm(request.POST or None, instance=obj, request=request) - msg_succ = _("{} unrecorded") if obj.recorded else _("{} recorded") - msg_succ = msg_succ.format(obj.identifier) - return form.process_request( - request, - msg_succ, - msg_error=_("Errors found:") - ) - - def post(self, request, id: str): - """ - - BaseModalForm provides the method process_request() which handles GET as well as POST requests. It was written - for easier handling of function based views. To support process_request() on class based views, the post() - call needs to be treated the same way as the get() call. - - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id - - """ - return self.get(request, id) + if obj.is_recorded: + return _("{} recorded").format(obj.identifier) + else: + return _("{} unrecorded").format(obj.identifier) -- 2.47.2 From c7a4c309bfb7622b6003d1c4a832d11019eec3b6 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 21 Oct 2025 08:48:30 +0200 Subject: [PATCH 22/36] # CompensationAction views refactored * refactors AbstractCompensationActionViews (new, edit, remove) to inherit from BaseModalFormView * refactors KOM, OEK, EMA views for compensation actions * moves message template strings into message_templates.py --- .../forms/modals/compensation_action.py | 9 +- .../tests/compensation/unit/test_forms.py | 12 ++- compensation/views/compensation/action.py | 46 ++------- compensation/views/eco_account/action.py | 38 ++------ ema/views/action.py | 41 +++----- konova/utils/message_templates.py | 4 + konova/views/action.py | 95 ++++--------------- konova/views/record.py | 7 +- 8 files changed, 68 insertions(+), 184 deletions(-) diff --git a/compensation/forms/modals/compensation_action.py b/compensation/forms/modals/compensation_action.py index 6c5e0666..54156b13 100644 --- a/compensation/forms/modals/compensation_action.py +++ b/compensation/forms/modals/compensation_action.py @@ -7,10 +7,12 @@ Created on: 18.08.22 """ from dal import autocomplete from django import forms +from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from codelist.models import KonovaCode from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID +from compensation.models import CompensationAction from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple from konova.forms.modals import BaseModalForm, RemoveModalForm from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, ADDED_COMPENSATION_ACTION @@ -114,7 +116,8 @@ class EditCompensationActionModalForm(NewCompensationActionModalForm): action = None def __init__(self, *args, **kwargs): - self.action = kwargs.pop("action", None) + action_id = kwargs.pop("action_id", None) + self.action = get_object_or_404(CompensationAction, id=action_id) super().__init__(*args, **kwargs) self.form_title = _("Edit action") form_data = { @@ -147,8 +150,8 @@ class RemoveCompensationActionModalForm(RemoveModalForm): action = None def __init__(self, *args, **kwargs): - action = kwargs.pop("action", None) - self.action = action + action_id = kwargs.pop("action_id", None) + self.action = get_object_or_404(CompensationAction, id=action_id) super().__init__(*args, **kwargs) def save(self): diff --git a/compensation/tests/compensation/unit/test_forms.py b/compensation/tests/compensation/unit/test_forms.py index 333ea947..dd804b77 100644 --- a/compensation/tests/compensation/unit/test_forms.py +++ b/compensation/tests/compensation/unit/test_forms.py @@ -80,7 +80,11 @@ class EditCompensationActionModalFormTestCase(NewCompensationActionModalFormTest self.compensation.actions.add(self.comp_action) def test_init(self): - form = EditCompensationActionModalForm(request=self.request, instance=self.compensation, action=self.comp_action) + form = EditCompensationActionModalForm( + request=self.request, + instance=self.compensation, + action_id=self.comp_action.id + ) self.assertEqual(form.form_title, str(_("Edit action"))) self.assertEqual(len(form.fields["action_type"].initial), self.comp_action.action_type.count()) self.assertEqual(len(form.fields["action_type_details"].initial), self.comp_action.action_type_details.count()) @@ -101,7 +105,7 @@ class EditCompensationActionModalFormTestCase(NewCompensationActionModalFormTest "comment": comment, } - form = EditCompensationActionModalForm(data, request=self.request, instance=self.compensation, action=self.comp_action) + form = EditCompensationActionModalForm(data, request=self.request, instance=self.compensation, action_id=self.comp_action.id) self.assertTrue(form.is_valid()) action = form.save() @@ -126,7 +130,7 @@ class RemoveCompensationActionModalFormTestCase(EditCompensationActionModalFormT def test_init(self): self.assertIn(self.comp_action, self.compensation.actions.all()) - form = RemoveCompensationActionModalForm(request=self.request, instance=self.compensation, action=self.comp_action) + form = RemoveCompensationActionModalForm(request=self.request, instance=self.compensation, action_id=self.comp_action.id) self.assertEqual(form.action, self.comp_action) def test_save(self): @@ -137,7 +141,7 @@ class RemoveCompensationActionModalFormTestCase(EditCompensationActionModalFormT data, request=self.request, instance=self.compensation, - action=self.comp_action + action_id=self.comp_action.id ) self.assertTrue(form.is_valid()) self.assertIn(self.comp_action, self.compensation.actions.all()) diff --git a/compensation/views/compensation/action.py b/compensation/views/compensation/action.py index aa87c71d..9b412d6f 100644 --- a/compensation/views/compensation/action.py +++ b/compensation/views/compensation/action.py @@ -5,53 +5,23 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse -from django.utils.decorators import method_decorator -from compensation.forms.modals.compensation_action import RemoveCompensationActionModalForm, \ - EditCompensationActionModalForm, NewCompensationActionModalForm -from compensation.models import Compensation, CompensationAction -from konova.decorators import shared_access_required, default_group_required, login_required_modal -from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_EDITED, \ - COMPENSATION_ACTION_ADDED +from compensation.models import Compensation from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ AbstractRemoveCompensationActionView +_COMPENSATION_DETAIL_URL_NAME = "compensation:detail" class NewCompensationActionView(AbstractNewCompensationActionView): - model = Compensation - redirect_url = "compensation:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME class EditCompensationActionView(AbstractEditCompensationActionView): - model = Compensation - redirect_url = "compensation:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME class RemoveCompensationActionView(AbstractRemoveCompensationActionView): - model = Compensation - redirect_url = "compensation:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME diff --git a/compensation/views/eco_account/action.py b/compensation/views/eco_account/action.py index 6aca3825..f94156a2 100644 --- a/compensation/views/eco_account/action.py +++ b/compensation/views/eco_account/action.py @@ -5,46 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - from compensation.models import EcoAccount -from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ AbstractRemoveCompensationActionView +_ECO_ACCOUNT_DETAIL_URL_NAME = "compensation:acc:detail" class NewEcoAccountActionView(AbstractNewCompensationActionView): - model = EcoAccount - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME class EditEcoAccountActionView(AbstractEditCompensationActionView): - model = EcoAccount - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME class RemoveEcoAccountActionView(AbstractRemoveCompensationActionView): - model = EcoAccount - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME diff --git a/ema/views/action.py b/ema/views/action.py index 068c224a..e09511fd 100644 --- a/ema/views/action.py +++ b/ema/views/action.py @@ -5,46 +5,31 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator from ema.models import Ema -from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ AbstractRemoveCompensationActionView +_EMA_ACCOUNT_DETAIL_URL_NAME = "ema:detail" class NewEmaActionView(AbstractNewCompensationActionView): - model = Ema - redirect_url = "ema:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(conservation_office_group_required) - @method_decorator(shared_access_required(Ema, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Ema + _REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME + def _user_has_permission(self, user): + return user.is_ets_user() class EditEmaActionView(AbstractEditCompensationActionView): - model = Ema - redirect_url = "ema:detail" + _MODEL_CLS = Ema + _REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME - @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) + def _user_has_permission(self, user): + return user.is_ets_user() class RemoveEmaActionView(AbstractRemoveCompensationActionView): - model = Ema - redirect_url = "ema:detail" + _MODEL_CLS = Ema + _REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME - @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) + def _user_has_permission(self, user): + return user.is_ets_user() diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py index ba44934b..09ead4da 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -19,7 +19,11 @@ IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since ano ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office users are allowed to remove entries.") MISSING_GROUP_PERMISSION = _("You need to be part of another user group.") CHECK_STATE_RESET = _("Status of Checked reset") + +# RECORDING RECORDED_BLOCKS_EDIT = _("Entry is recorded. To edit data, the entry first needs to be unrecorded.") +ENTRY_RECORDED = _("{} recorded") +ENTRY_UNRECORDED = _("{} unrecorded") # SHARE DATA_UNSHARED = _("This data is not shared with you") diff --git a/konova/views/action.py b/konova/views/action.py index 5699a963..cd00187d 100644 --- a/konova/views/action.py +++ b/konova/views/action.py @@ -5,104 +5,47 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 22.08.22 """ -from django.shortcuts import get_object_or_404 -from django.urls import reverse -from django.views import View +from django.contrib.auth.mixins import LoginRequiredMixin from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \ EditCompensationActionModalForm, RemoveCompensationActionModalForm -from compensation.models import CompensationAction from konova.utils.message_templates import COMPENSATION_STATE_ADDED, COMPENSATION_STATE_EDITED, \ COMPENSATION_STATE_REMOVED +from konova.views.base import BaseModalFormView -class AbstractCompensationActionView(View): - model = None - redirect_url = None +class AbstractCompensationActionView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = None + _REDIRECT_URL = None class Meta: abstract = True + def _user_has_permission(self, user): + return user.is_default_user() + + def _get_redirect_url(self, *args, **kwargs): + return super()._get_redirect_url(*args, **kwargs) + "#related_data" class AbstractNewCompensationActionView(AbstractCompensationActionView): + _FORM_CLS = NewCompensationActionModalForm + _MSG_SUCCESS = COMPENSATION_STATE_ADDED + class Meta: abstract = True - def get(self, request, id: str): - """ Renders a form for adding new actions - - Args: - request (HttpRequest): The incoming request - id (str): The object's id to which the new action will be related - - Returns: - - """ - obj = get_object_or_404(self.model, id=id) - form = NewCompensationActionModalForm(request.POST or None, instance=obj, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_ADDED, - redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" - ) - - def post(self, request, id: str): - return self.get(request, id) - class AbstractEditCompensationActionView(AbstractCompensationActionView): + _FORM_CLS = EditCompensationActionModalForm + _MSG_SUCCESS = COMPENSATION_STATE_EDITED + class Meta: abstract = True - - def get(self, request, id: str, action_id: str): - """ Renders a form for editing a action - - Args: - request (HttpRequest): The incoming request - id (str): The object id - action_id (str): The action's id - - Returns: - - """ - obj = get_object_or_404(self.model, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = EditCompensationActionModalForm(request.POST or None, instance=obj, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_EDITED, - redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" - ) - - def post(self, request, id: str, action_id: str): - return self.get(request, id, action_id) class AbstractRemoveCompensationActionView(AbstractCompensationActionView): + _FORM_CLS = RemoveCompensationActionModalForm + _MSG_SUCCESS = COMPENSATION_STATE_REMOVED + class Meta: abstract = True - - def get(self, request, id: str, action_id: str): - """ Renders a form for removing aaction - - Args: - request (HttpRequest): The incoming request - id (str): The object id - action_id (str): The action's id - - Returns: - - """ - obj = get_object_or_404(self.model, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = RemoveCompensationActionModalForm(request.POST or None, instance=obj, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_REMOVED, - redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" - ) - - def post(self, request, id: str, action_id: str): - return self.get(request, id, action_id) - - diff --git a/konova/views/record.py b/konova/views/record.py index f2c09640..7f3c41ce 100644 --- a/konova/views/record.py +++ b/konova/views/record.py @@ -5,9 +5,8 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.utils.translation import gettext_lazy as _ - from konova.forms.modals import RecordModalForm +from konova.utils.message_templates import ENTRY_RECORDED, ENTRY_UNRECORDED from konova.views.base import BaseModalFormView @@ -24,6 +23,6 @@ class AbstractRecordView(BaseModalFormView): assert obj is not None if obj.is_recorded: - return _("{} recorded").format(obj.identifier) + return ENTRY_RECORDED.format(obj.identifier) else: - return _("{} unrecorded").format(obj.identifier) + return ENTRY_UNRECORDED.format(obj.identifier) -- 2.47.2 From 6056a8882dddc707e21b7da79abdc08b31b386a5 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 21 Oct 2025 09:04:57 +0200 Subject: [PATCH 23/36] # Deadline views refactored * refactors AbstractDeadlineViews (new, edit, remove) to inherit from BaseModalFormView * refactors KOM, OEK, EMA views for deadline views --- compensation/forms/modals/deadline.py | 6 +- compensation/views/compensation/deadline.py | 38 ++------ compensation/views/eco_account/deadline.py | 37 ++----- ema/views/deadline.py | 42 +++----- konova/forms/modals/remove_form.py | 7 +- konova/views/deadline.py | 103 ++++++-------------- 6 files changed, 64 insertions(+), 169 deletions(-) diff --git a/compensation/forms/modals/deadline.py b/compensation/forms/modals/deadline.py index 12baebad..e34e6a59 100644 --- a/compensation/forms/modals/deadline.py +++ b/compensation/forms/modals/deadline.py @@ -6,10 +6,11 @@ Created on: 18.08.22 """ from django import forms +from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from konova.forms.modals import BaseModalForm -from konova.models import DeadlineType +from konova.models import DeadlineType, Deadline from konova.utils import validators from konova.utils.message_templates import DEADLINE_EDITED @@ -90,7 +91,8 @@ class EditDeadlineModalForm(NewDeadlineModalForm): deadline = None def __init__(self, *args, **kwargs): - self.deadline = kwargs.pop("deadline", None) + deadline_id = kwargs.pop("deadline_id", None) + self.deadline = get_object_or_404(Deadline, id=deadline_id) super().__init__(*args, **kwargs) self.form_title = _("Edit deadline") form_data = { diff --git a/compensation/views/compensation/deadline.py b/compensation/views/compensation/deadline.py index 7e2a9fb3..88786360 100644 --- a/compensation/views/compensation/deadline.py +++ b/compensation/views/compensation/deadline.py @@ -5,45 +5,21 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - from compensation.models import Compensation -from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.deadline import AbstractRemoveDeadlineView, AbstractEditDeadlineView, AbstractNewDeadlineView +_COMPENSATION_DETAIL_URL_NAME = "compensation:detail" class NewCompensationDeadlineView(AbstractNewDeadlineView): - model = Compensation - redirect_url = "compensation:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME class EditCompensationDeadlineView(AbstractEditDeadlineView): - model = Compensation - redirect_url = "compensation:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME class RemoveCompensationDeadlineView(AbstractRemoveDeadlineView): - model = Compensation - redirect_url = "compensation:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME diff --git a/compensation/views/eco_account/deadline.py b/compensation/views/eco_account/deadline.py index c49dba35..ccc66717 100644 --- a/compensation/views/eco_account/deadline.py +++ b/compensation/views/eco_account/deadline.py @@ -5,45 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - from compensation.models import EcoAccount -from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.deadline import AbstractNewDeadlineView, AbstractEditDeadlineView, AbstractRemoveDeadlineView +_ECO_ACCOUNT_DETAIL_URL_NAME = "compensation:acc:detail" class NewEcoAccountDeadlineView(AbstractNewDeadlineView): - model = EcoAccount - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME class EditEcoAccountDeadlineView(AbstractEditDeadlineView): - model = EcoAccount - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME class RemoveEcoAccountDeadlineView(AbstractRemoveDeadlineView): - model = EcoAccount - redirect_url = "compensation:acc:detail" + _MODEL_CLS = EcoAccount + _REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) diff --git a/ema/views/deadline.py b/ema/views/deadline.py index d760bdab..475d1ffa 100644 --- a/ema/views/deadline.py +++ b/ema/views/deadline.py @@ -5,46 +5,30 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - from ema.models import Ema -from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.views.deadline import AbstractNewDeadlineView, AbstractRemoveDeadlineView, AbstractEditDeadlineView +_EMA_DETAIL_URL_NAME = "ema:detail" class NewEmaDeadlineView(AbstractNewDeadlineView): - model = Ema - redirect_url = "ema:detail" + _MODEL_CLS = Ema + _REDIRECT_URL = _EMA_DETAIL_URL_NAME - @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) + def _user_has_permission(self, user): + return user.is_ets_user() class EditEmaDeadlineView(AbstractEditDeadlineView): - model = Ema - redirect_url = "ema:detail" + _MODEL_CLS = Ema + _REDIRECT_URL = _EMA_DETAIL_URL_NAME - @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) + def _user_has_permission(self, user): + return user.is_ets_user() class RemoveEmaDeadlineView(AbstractRemoveDeadlineView): - model = Ema - redirect_url = "ema:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(conservation_office_group_required) - @method_decorator(shared_access_required(Ema, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Ema + _REDIRECT_URL = _EMA_DETAIL_URL_NAME + def _user_has_permission(self, user): + return user.is_ets_user() diff --git a/konova/forms/modals/remove_form.py b/konova/forms/modals/remove_form.py index f09af99a..d89d2e52 100644 --- a/konova/forms/modals/remove_form.py +++ b/konova/forms/modals/remove_form.py @@ -6,10 +6,11 @@ Created on: 15.08.22 """ from django import forms +from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from konova.forms.modals.base_form import BaseModalForm -from konova.models import BaseObject +from konova.models import BaseObject, Deadline class RemoveModalForm(BaseModalForm): @@ -51,8 +52,8 @@ class RemoveDeadlineModalForm(RemoveModalForm): deadline = None def __init__(self, *args, **kwargs): - deadline = kwargs.pop("deadline", None) - self.deadline = deadline + deadline_id = kwargs.pop("deadline_id", None) + self.deadline = get_object_or_404(Deadline, id=deadline_id) super().__init__(*args, **kwargs) def save(self): diff --git a/konova/views/deadline.py b/konova/views/deadline.py index 4350073d..382c5f77 100644 --- a/konova/views/deadline.py +++ b/konova/views/deadline.py @@ -5,102 +5,57 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 22.08.22 """ -from django.shortcuts import get_object_or_404 -from django.urls import reverse -from django.views import View +from django.contrib.auth.mixins import LoginRequiredMixin from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm from konova.forms.modals import RemoveDeadlineModalForm -from konova.models import Deadline from konova.utils.message_templates import DEADLINE_ADDED, DEADLINE_EDITED, DEADLINE_REMOVED +from konova.views.base import BaseModalFormView -class AbstractNewDeadlineView(View): - model = None - redirect_url = None +class AbstractNewDeadlineView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = None + _FORM_CLS = NewDeadlineModalForm + _REDIRECT_URL = None + _MSG_SUCCESS = DEADLINE_ADDED class Meta: abstract = True - def get(self, request, id: str): - """ Renders a form for adding new deadlines + def _get_redirect_url(self, *args, **kwargs): + return super()._get_redirect_url(*args, **kwargs) + "#related_data" - Args: - request (HttpRequest): The incoming request - id (str): The account's id to which the new state will be related - - Returns: - - """ - obj = get_object_or_404(self.model, id=id) - form = NewDeadlineModalForm(request.POST or None, instance=obj, request=request) - return form.process_request( - request, - msg_success=DEADLINE_ADDED, - redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" - ) - - def post(self, request, id: str): - return self.get(request, id) + def _user_has_permission(self, user): + return user.is_default_user() -class AbstractEditDeadlineView(View): - model = None - redirect_url = None +class AbstractEditDeadlineView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = None + _FORM_CLS = EditDeadlineModalForm + _REDIRECT_URL = None + _MSG_SUCCESS = DEADLINE_EDITED class Meta: abstract = True - def get(self, request, id: str, deadline_id: str): - """ Renders a form for editing deadlines + def _get_redirect_url(self, *args, **kwargs): + return super()._get_redirect_url(*args, **kwargs) + "#related_data" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - obj = get_object_or_404(self.model, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = EditDeadlineModalForm(request.POST or None, instance=obj, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_EDITED, - redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" - ) - - def post(self, request, id: str, deadline_id: str): - return self.get(request, id, deadline_id) + def _user_has_permission(self, user): + return user.is_default_user() -class AbstractRemoveDeadlineView(View): - model = None - redirect_url = None +class AbstractRemoveDeadlineView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = None + _FORM_CLS = RemoveDeadlineModalForm + _REDIRECT_URL = None + _MSG_SUCCESS = DEADLINE_REMOVED class Meta: abstract = True - def get(self, request, id: str, deadline_id: str): - """ Renders a form for removing deadlines + def _get_redirect_url(self, *args, **kwargs): + return super()._get_redirect_url(*args, **kwargs) + "#related_data" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - obj = get_object_or_404(self.model, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = RemoveDeadlineModalForm(request.POST or None, instance=obj, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_REMOVED, - redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" - ) - - def post(self, request, id: str, deadline_id: str): - return self.get(request, id, deadline_id) + def _user_has_permission(self, user): + return user.is_default_user() -- 2.47.2 From d5accb214359bea5afc2098de4071dcbdd607671 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 21 Oct 2025 09:14:46 +0200 Subject: [PATCH 24/36] # Deadline tests refactored * refactors tests for deadline views to check whether they work properly --- compensation/tests/compensation/unit/test_models.py | 2 +- konova/tests/unit/test_deadline.py | 2 +- konova/tests/unit/test_forms.py | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/compensation/tests/compensation/unit/test_models.py b/compensation/tests/compensation/unit/test_models.py index cb85a4b5..274c7b93 100644 --- a/compensation/tests/compensation/unit/test_models.py +++ b/compensation/tests/compensation/unit/test_models.py @@ -36,7 +36,7 @@ class AbstractCompensationModelTestCase(BaseTestCase): data, request=self.request, instance=self.compensation, - deadline=self.finished_deadline, + deadline_id=self.finished_deadline.id, ) self.assertTrue(form.is_valid(), msg=form.errors) self.assertIn(self.finished_deadline, self.compensation.deadlines.all()) diff --git a/konova/tests/unit/test_deadline.py b/konova/tests/unit/test_deadline.py index 1da98010..5bad8fe1 100644 --- a/konova/tests/unit/test_deadline.py +++ b/konova/tests/unit/test_deadline.py @@ -103,7 +103,7 @@ class EditDeadlineModalFormTestCase(NewDeadlineModalFormTestCase): data, request=self.request, instance=self.compensation, - deadline=self.finished_deadline, + deadline_id=self.finished_deadline.id, ) self.assertTrue(form.is_valid(), msg=form.errors) diff --git a/konova/tests/unit/test_forms.py b/konova/tests/unit/test_forms.py index 190a756e..453adb9e 100644 --- a/konova/tests/unit/test_forms.py +++ b/konova/tests/unit/test_forms.py @@ -256,7 +256,7 @@ class RemoveDeadlineTestCase(BaseTestCase): form = RemoveDeadlineModalForm( request=self.request, instance=self.compensation, - deadline=self.finished_deadline + deadline_id=self.finished_deadline.id ) self.assertEqual(form.form_title, str(_("Remove"))) self.assertEqual(form.form_caption, str(_("Are you sure?"))) @@ -273,7 +273,7 @@ class RemoveDeadlineTestCase(BaseTestCase): data, request=self.request, instance=self.compensation, - deadline=self.finished_deadline + deadline_id=self.finished_deadline.id ) self.assertTrue(form.is_valid(), msg=form.errors) form.save() -- 2.47.2 From d2a57df08047d1fadb12e97aafbb1bf7876c1379 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 21 Oct 2025 13:48:16 +0200 Subject: [PATCH 25/36] # Document views refactoring * refactors new, edit, get and delete views for eiv, kom, oek and ema * introduces --- compensation/forms/modals/document.py | 21 ++- compensation/views/compensation/document.py | 59 +++------ compensation/views/eco_account/document.py | 62 +++------ ema/forms.py | 10 +- ema/views/document.py | 65 ++++------ intervention/forms/modals/document.py | 32 ++++- intervention/views/document.py | 58 +++------ konova/forms/modals/document_form.py | 18 +-- konova/forms/modals/remove_form.py | 12 +- konova/tests/unit/test_forms.py | 9 +- konova/utils/documents.py | 27 ---- konova/views/base.py | 21 ++- konova/views/document.py | 137 ++++++++------------ 13 files changed, 219 insertions(+), 312 deletions(-) diff --git a/compensation/forms/modals/document.py b/compensation/forms/modals/document.py index 83c2ff92..2cdd1cff 100644 --- a/compensation/forms/modals/document.py +++ b/compensation/forms/modals/document.py @@ -6,12 +6,27 @@ Created on: 18.08.22 """ from compensation.models import CompensationDocument, EcoAccountDocument -from konova.forms.modals import NewDocumentModalForm +from konova.forms.modals import NewDocumentModalForm, EditDocumentModalForm, RemoveDocumentModalForm class NewCompensationDocumentModalForm(NewDocumentModalForm): - document_model = CompensationDocument + _DOCUMENT_CLS = CompensationDocument + + +class EditCompensationDocumentModalForm(EditDocumentModalForm): + _DOCUMENT_CLS = CompensationDocument + + +class RemoveCompensationDocumentModalForm(RemoveDocumentModalForm): + _DOCUMENT_CLS = CompensationDocument class NewEcoAccountDocumentModalForm(NewDocumentModalForm): - document_model = EcoAccountDocument + _DOCUMENT_CLS = EcoAccountDocument + +class EditEcoAccountDocumentModalForm(EditDocumentModalForm): + _DOCUMENT_CLS = EcoAccountDocument + +class RemoveEcoAccountDocumentModalForm(RemoveDocumentModalForm): + _DOCUMENT_CLS = EcoAccountDocument + diff --git a/compensation/views/compensation/document.py b/compensation/views/compensation/document.py index cb7de2a8..257d6e13 100644 --- a/compensation/views/compensation/document.py +++ b/compensation/views/compensation/document.py @@ -5,62 +5,33 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - -from compensation.forms.modals.document import NewCompensationDocumentModalForm +from compensation.forms.modals.document import NewCompensationDocumentModalForm, EditCompensationDocumentModalForm, \ + RemoveCompensationDocumentModalForm from compensation.models import Compensation, CompensationDocument -from konova.decorators import shared_access_required, default_group_required, login_required_modal -from konova.forms.modals import EditDocumentModalForm from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ AbstractEditDocumentView class NewCompensationDocumentView(AbstractNewDocumentView): - model = Compensation - form = NewCompensationDocumentModalForm - redirect_url = "compensation:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _FORM_CLS = NewCompensationDocumentModalForm + _REDIRECT_URL = "compensation:detail" class GetCompensationDocumentView(AbstractGetDocumentView): - model = Compensation - document_model = CompensationDocument - - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _DOCUMENT_CLS = CompensationDocument class RemoveCompensationDocumentView(AbstractRemoveDocumentView): - model = Compensation - document_model = CompensationDocument - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _DOCUMENT_CLS = CompensationDocument + _FORM_CLS = RemoveCompensationDocumentModalForm + _REDIRECT_URL = "compensation:detail" class EditCompensationDocumentView(AbstractEditDocumentView): - model = Compensation - document_model = CompensationDocument - form = EditDocumentModalForm - redirect_url = "compensation:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _DOCUMENT_CLS = CompensationDocument + _FORM_CLS = EditCompensationDocumentModalForm + _REDIRECT_URL = "compensation:detail" diff --git a/compensation/views/eco_account/document.py b/compensation/views/eco_account/document.py index 73fdcd44..3d3919f3 100644 --- a/compensation/views/eco_account/document.py +++ b/compensation/views/eco_account/document.py @@ -5,65 +5,33 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 -from django.urls import reverse -from django.utils.decorators import method_decorator - -from compensation.forms.modals.document import NewEcoAccountDocumentModalForm +from compensation.forms.modals.document import NewEcoAccountDocumentModalForm, RemoveEcoAccountDocumentModalForm, \ + EditEcoAccountDocumentModalForm from compensation.models import EcoAccount, EcoAccountDocument -from konova.decorators import shared_access_required, default_group_required, login_required_modal -from konova.forms.modals import EditDocumentModalForm from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ AbstractEditDocumentView class NewEcoAccountDocumentView(AbstractNewDocumentView): - model = EcoAccount - form = NewEcoAccountDocumentModalForm - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _FORM_CLS = NewEcoAccountDocumentModalForm + _REDIRECT_URL = "compensation:acc:detail" class GetEcoAccountDocumentView(AbstractGetDocumentView): - model = EcoAccount - document_model = EcoAccountDocument - - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _DOCUMENT_CLS = EcoAccountDocument class RemoveEcoAccountDocumentView(AbstractRemoveDocumentView): - model = EcoAccount - document_model = EcoAccountDocument - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _DOCUMENT_CLS = EcoAccountDocument + _FORM_CLS = RemoveEcoAccountDocumentModalForm + _REDIRECT_URL = "compensation:acc:detail" class EditEcoAccountDocumentView(AbstractEditDocumentView): - model = EcoAccount - document_model = EcoAccountDocument - form = EditDocumentModalForm - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _DOCUMENT_CLS = EcoAccountDocument + _FORM_CLS = EditEcoAccountDocumentModalForm + _REDIRECT_URL = "compensation:acc:detail" diff --git a/ema/forms.py b/ema/forms.py index 26bbc2db..5a98a903 100644 --- a/ema/forms.py +++ b/ema/forms.py @@ -15,7 +15,7 @@ from compensation.forms.compensation import AbstractCompensationForm from ema.models import Ema, EmaDocument from intervention.models import Responsibility, Handler from konova.forms import SimpleGeomForm -from konova.forms.modals import NewDocumentModalForm +from konova.forms.modals import NewDocumentModalForm, EditDocumentModalForm, RemoveDocumentModalForm from user.models import UserActionLogEntry @@ -170,4 +170,10 @@ class EditEmaForm(NewEmaForm): class NewEmaDocumentModalForm(NewDocumentModalForm): - document_model = EmaDocument \ No newline at end of file + _DOCUMENT_CLS = EmaDocument + +class EditEmaDocumentModalForm(EditDocumentModalForm): + _DOCUMENT_CLS = EmaDocument + +class RemoveEmaDocumentModalForm(RemoveDocumentModalForm): + _DOCUMENT_CLS = EmaDocument \ No newline at end of file diff --git a/ema/views/document.py b/ema/views/document.py index 45c58146..6f94723a 100644 --- a/ema/views/document.py +++ b/ema/views/document.py @@ -5,62 +5,41 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - -from ema.forms import NewEmaDocumentModalForm +from ema.forms import NewEmaDocumentModalForm, RemoveEmaDocumentModalForm, EditEmaDocumentModalForm from ema.models import Ema, EmaDocument -from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal -from konova.forms.modals import EditDocumentModalForm from konova.views.document import AbstractEditDocumentView, AbstractRemoveDocumentView, AbstractGetDocumentView, \ AbstractNewDocumentView class NewEmaDocumentView(AbstractNewDocumentView): - model = Ema - form = NewEmaDocumentModalForm - redirect_url = "ema:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(conservation_office_group_required) - @method_decorator(shared_access_required(Ema, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Ema + _FORM_CLS = NewEmaDocumentModalForm + _REDIRECT_URL = "ema:detail" + def _user_has_permission(self, user): + return user.is_ets_user() class GetEmaDocumentView(AbstractGetDocumentView): - model = Ema - document_model = EmaDocument - - @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) + _MODEL_CLS = Ema + _DOCUMENT_CLS = EmaDocument + def _user_has_permission(self, user): + return user.is_ets_user() class RemoveEmaDocumentView(AbstractRemoveDocumentView): - model = Ema - document_model = EmaDocument - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(conservation_office_group_required) - @method_decorator(shared_access_required(Ema, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Ema + _DOCUMENT_CLS = EmaDocument + _FORM_CLS = RemoveEmaDocumentModalForm + _REDIRECT_URL = "ema:detail" + def _user_has_permission(self, user): + return user.is_ets_user() class EditEmaDocumentView(AbstractEditDocumentView): - model = Ema - document_model = EmaDocument - form = EditDocumentModalForm - redirect_url = "ema:detail" + _MODEL_CLS = Ema + _FORM_CLS = EditEmaDocumentModalForm + _DOCUMENT_CLS = EmaDocument + _REDIRECT_URL = "ema:detail" - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(conservation_office_group_required) - @method_decorator(shared_access_required(Ema, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + def _user_has_permission(self, user): + return user.is_ets_user() diff --git a/intervention/forms/modals/document.py b/intervention/forms/modals/document.py index 0daf3eb1..1501cbcb 100644 --- a/intervention/forms/modals/document.py +++ b/intervention/forms/modals/document.py @@ -6,11 +6,11 @@ Created on: 18.08.22 """ from intervention.models import InterventionDocument -from konova.forms.modals import NewDocumentModalForm +from konova.forms.modals import NewDocumentModalForm, EditDocumentModalForm, RemoveDocumentModalForm class NewInterventionDocumentModalForm(NewDocumentModalForm): - document_model = InterventionDocument + _DOCUMENT_CLS = InterventionDocument def save(self, *args, **kwargs): """ Extension of regular NewDocumentModalForm @@ -28,3 +28,31 @@ class NewInterventionDocumentModalForm(NewDocumentModalForm): self.instance.send_data_to_egon() return doc + +class EditInterventionDocumentModalForm(EditDocumentModalForm): + _DOCUMENT_CLS = InterventionDocument + + def save(self, *args, **kwargs): + """ Extension of regular EditDocumentModalForm + + Checks whether payments exist on the intervention and sends the data to EGON + + Args: + *args (): + **kwargs (): + + Returns: + + """ + doc = super().save(*args, **kwargs) + self.instance.send_data_to_egon() + + return doc + + +class RemoveInterventionDocumentModalForm(RemoveDocumentModalForm): + _DOCUMENT_CLS = InterventionDocument + + def save(self, *args, **kwargs): + super().save(*args, **kwargs) + self.instance.send_data_to_egon() diff --git a/intervention/views/document.py b/intervention/views/document.py index 80612781..1cc08cf4 100644 --- a/intervention/views/document.py +++ b/intervention/views/document.py @@ -5,59 +5,33 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - -from intervention.forms.modals.document import NewInterventionDocumentModalForm +from intervention.forms.modals.document import NewInterventionDocumentModalForm, EditInterventionDocumentModalForm, \ + RemoveInterventionDocumentModalForm from intervention.models import Intervention, InterventionDocument -from konova.decorators import default_group_required, shared_access_required -from konova.forms.modals import EditDocumentModalForm from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ AbstractEditDocumentView class NewInterventionDocumentView(AbstractNewDocumentView): - model = Intervention - form = NewInterventionDocumentModalForm - redirect_url = "intervention:detail" - - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Intervention, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Intervention + _DOCUMENT_MODEL = InterventionDocument + _FORM_CLS = NewInterventionDocumentModalForm + _REDIRECT_URL = "intervention:detail" class GetInterventionDocumentView(AbstractGetDocumentView): - model = Intervention - document_model = InterventionDocument - - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Intervention, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Intervention + _DOCUMENT_CLS = InterventionDocument class RemoveInterventionDocumentView(AbstractRemoveDocumentView): - model = Intervention - document_model = InterventionDocument - - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Intervention, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) - + _MODEL_CLS = Intervention + _DOCUMENT_CLS = InterventionDocument + _FORM_CLS = RemoveInterventionDocumentModalForm + _REDIRECT_URL = "intervention:detail" class EditInterventionDocumentView(AbstractEditDocumentView): - model = Intervention - document_model = InterventionDocument - form = EditDocumentModalForm - redirect_url = "intervention:detail" - - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Intervention, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Intervention + _DOCUMENT_CLS = InterventionDocument + _FORM_CLS = EditInterventionDocumentModalForm + _REDIRECT_URL = "intervention:detail" diff --git a/konova/forms/modals/document_form.py b/konova/forms/modals/document_form.py index 427ef9f7..aaf1e6e7 100644 --- a/konova/forms/modals/document_form.py +++ b/konova/forms/modals/document_form.py @@ -8,10 +8,10 @@ Created on: 15.08.22 from django import forms from django.db import transaction from django.db.models.fields.files import FieldFile +from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from konova.forms.modals.base_form import BaseModalForm -from konova.models import AbstractDocument from konova.utils import validators from konova.utils.message_templates import DOCUMENT_EDITED, FILE_SIZE_TOO_LARGE, FILE_TYPE_UNSUPPORTED from user.models import UserActionLogEntry @@ -69,7 +69,7 @@ class NewDocumentModalForm(BaseModalForm): } ) ) - document_model = None + _DOCUMENT_CLS = None class Meta: abstract = True @@ -81,7 +81,7 @@ class NewDocumentModalForm(BaseModalForm): self.form_attrs = { "enctype": "multipart/form-data", # important for file upload } - if not self.document_model: + if not self._DOCUMENT_CLS: raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__)) def is_valid(self): @@ -93,14 +93,14 @@ class NewDocumentModalForm(BaseModalForm): # FieldFile declares that no new file has been uploaded and we do not need to check on the file again return super_valid - mime_type_valid = self.document_model.is_mime_type_valid(_file) + mime_type_valid = self._DOCUMENT_CLS.is_mime_type_valid(_file) if not mime_type_valid: self.add_error( "file", FILE_TYPE_UNSUPPORTED ) - file_size_valid = self.document_model.is_file_size_valid(_file) + file_size_valid = self._DOCUMENT_CLS.is_file_size_valid(_file) if not file_size_valid: self.add_error( "file", @@ -115,7 +115,7 @@ class NewDocumentModalForm(BaseModalForm): action = UserActionLogEntry.get_created_action(self.user) edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document")) - doc = self.document_model.objects.create( + doc = self._DOCUMENT_CLS.objects.create( created=action, title=self.cleaned_data["title"], comment=self.cleaned_data["comment"], @@ -133,10 +133,12 @@ class NewDocumentModalForm(BaseModalForm): class EditDocumentModalForm(NewDocumentModalForm): document = None - document_model = AbstractDocument + _DOCUMENT_CLS = None def __init__(self, *args, **kwargs): - self.document = kwargs.pop("document", None) + doc_id = kwargs.pop("doc_id", None) + self.document = get_object_or_404(self._DOCUMENT_CLS, id=doc_id) + super().__init__(*args, **kwargs) self.form_title = _("Edit document") form_data = { diff --git a/konova/forms/modals/remove_form.py b/konova/forms/modals/remove_form.py index d89d2e52..d4c36f81 100644 --- a/konova/forms/modals/remove_form.py +++ b/konova/forms/modals/remove_form.py @@ -57,4 +57,14 @@ class RemoveDeadlineModalForm(RemoveModalForm): super().__init__(*args, **kwargs) def save(self): - self.instance.remove_deadline(self) \ No newline at end of file + self.instance.remove_deadline(self) + + +class RemoveDocumentModalForm(RemoveModalForm): + instance = None + _DOCUMENT_CLS = None + + def __init__(self, *args, **kwargs): + document_id = kwargs.pop("doc_id", None) + super().__init__(*args, **kwargs) + self.instance = get_object_or_404(self._DOCUMENT_CLS, id=document_id) diff --git a/konova/tests/unit/test_forms.py b/konova/tests/unit/test_forms.py index 453adb9e..188710da 100644 --- a/konova/tests/unit/test_forms.py +++ b/konova/tests/unit/test_forms.py @@ -17,9 +17,9 @@ from django.utils.timezone import now from compensation.forms.modals.document import NewEcoAccountDocumentModalForm, NewCompensationDocumentModalForm from compensation.models import Payment from ema.forms import NewEmaDocumentModalForm -from intervention.forms.modals.document import NewInterventionDocumentModalForm +from intervention.forms.modals.document import NewInterventionDocumentModalForm, EditInterventionDocumentModalForm from intervention.models import InterventionDocument -from konova.forms.modals import EditDocumentModalForm, NewDocumentModalForm, RecordModalForm, RemoveModalForm, \ +from konova.forms.modals import NewDocumentModalForm, RecordModalForm, RemoveModalForm, \ RemoveDeadlineModalForm, ResubmissionModalForm from konova.models import Resubmission from konova.tests.test_views import BaseTestCase @@ -106,12 +106,12 @@ class EditDocumentModalFormTestCase(NewDocumentModalFormTestCase): InterventionDocument, instance=self.intervention ) - self.form = EditDocumentModalForm( + self.form = EditInterventionDocumentModalForm( self.data, dummy_file_dict, request=self.request, instance=self.intervention, - document=self.doc + doc_id=self.doc.id ) def test_init(self): @@ -122,7 +122,6 @@ class EditDocumentModalFormTestCase(NewDocumentModalFormTestCase): self.assertEqual(self.form.fields["title"].initial, self.doc.title) self.assertEqual(self.form.fields["comment"].initial, self.doc.comment) self.assertEqual(self.form.fields["creation_date"].initial, self.doc.date_of_creation) - self.assertEqual(self.form.fields["file"].initial, self.doc.file) def test_save(self): self.assertTrue(self.form.is_valid(), msg=self.form.errors) diff --git a/konova/utils/documents.py b/konova/utils/documents.py index 3e8f6f12..7c38b685 100644 --- a/konova/utils/documents.py +++ b/konova/utils/documents.py @@ -7,9 +7,7 @@ Created on: 01.09.21 """ from django.http import FileResponse, HttpRequest, Http404 -from konova.forms.modals import RemoveModalForm from konova.models import AbstractDocument -from konova.utils.message_templates import DOCUMENT_REMOVED_TEMPLATE def get_document(doc: AbstractDocument): @@ -26,28 +24,3 @@ def get_document(doc: AbstractDocument): return FileResponse(doc.file, as_attachment=True) except FileNotFoundError: raise Http404() - - -def remove_document(request: HttpRequest, doc: AbstractDocument): - """ Renders a form for uploading new documents - - This function works using a modal. We are not using the regular way, the django bootstrap modal forms are - intended to be used. Instead of View classes we work using the classic way of dealing with forms (see below). - It is important to mention, that modal forms, which should reload the page afterwards, must provide a - 'reload_page' bool in the context. This way, the modal may reload the page or not. - - For further details see the comments in templates/modal or - https://github.com/trco/django-bootstrap-modal-forms - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - title = doc.title - form = RemoveModalForm(request.POST or None, instance=doc, request=request) - return form.process_request( - request=request, - msg_success=DOCUMENT_REMOVED_TEMPLATE.format(title), - ) \ No newline at end of file diff --git a/konova/views/base.py b/konova/views/base.py index d8b1ab0b..3c5d8499 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -89,7 +89,13 @@ class BaseModalFormView(BaseView): def get(self, request: HttpRequest, id: str, *args, **kwargs): obj = self._MODEL_CLS.objects.get(id=id) - form = self._FORM_CLS(request.POST or None, instance=obj, request=request, **kwargs) + form = self._FORM_CLS( + request.POST or None, + request.FILES or None, + instance=obj, + request=request, + **kwargs + ) context = { "form": form, } @@ -98,18 +104,25 @@ class BaseModalFormView(BaseView): def post(self, request: HttpRequest, id: str, *args, **kwargs): obj = self._MODEL_CLS.objects.get(id=id) - form = self._FORM_CLS(request.POST or None, instance=obj, request=request, **kwargs) + form = self._FORM_CLS( + request.POST or None, + request.FILES or None, + instance=obj, + request=request, + **kwargs + ) redirect_url = self._get_redirect_url(obj=obj) if form.is_valid(): if not is_ajax(request.META): # Modal forms send one POST for checking on data validity. This can be used to return possible errors - # on the form. A second POST (if no errors occured) is sent afterwards and needs to process the + # on the form. A second POST (if no errors occurs) is sent afterward and needs to process the # saving/commiting of the data to the database. is_ajax() performs this check. The first request is # an ajax call, the second is a regular form POST. + msg_success = self._get_msg_success(obj=obj, *args, **kwargs) form.save() messages.success( request, - self._get_msg_success(obj=obj) + msg_success ) return HttpResponseRedirect(redirect_url) else: diff --git a/konova/views/document.py b/konova/views/document.py index 8dba6fd5..09df39c7 100644 --- a/konova/views/document.py +++ b/konova/views/document.py @@ -5,46 +5,35 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 22.08.22 """ +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest from django.shortcuts import get_object_or_404 -from django.urls import reverse -from django.views import View -from konova.utils.documents import get_document, remove_document -from konova.utils.message_templates import DOCUMENT_ADDED, DOCUMENT_EDITED +from konova.forms.modals import EditDocumentModalForm +from konova.utils.documents import get_document +from konova.utils.message_templates import DOCUMENT_ADDED, DOCUMENT_EDITED, DOCUMENT_REMOVED_TEMPLATE +from konova.views.base import BaseModalFormView, BaseView -class AbstractNewDocumentView(View): - model = None - form = None - redirect_url = None +class AbstractNewDocumentView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = None + _FORM_CLS = None + _REDIRECT_URL = None + _MSG_SUCCESS = DOCUMENT_ADDED class Meta: abstract = True - def get(self, request, id: str): - """ Renders a form for uploading new documents + def _get_redirect_url(self, *args, **kwargs): + return super()._get_redirect_url(*args, **kwargs) + "#related_data" - Args: - request (HttpRequest): The incoming request - id (str): The object's id to which the new document will be related - Returns: - - """ - intervention = get_object_or_404(self.model, id=id) - form = self.form(request.POST or None, request.FILES or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=DOCUMENT_ADDED, - redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" - ) - - def post(self, request, id: str): - return self.get(request, id) + def _user_has_permission(self, user): + return user.is_default_user() -class AbstractGetDocumentView(View): - model = None - document_model = None +class AbstractGetDocumentView(LoginRequiredMixin, BaseView): + _MODEL_CLS = None + _DOCUMENT_CLS = None class Meta: abstract = True @@ -62,77 +51,57 @@ class AbstractGetDocumentView(View): Returns: """ - get_object_or_404(self.model, id=id) - doc = get_object_or_404(self.document_model, id=doc_id) + get_object_or_404(self._MODEL_CLS, id=id) + doc = get_object_or_404(self._DOCUMENT_CLS, id=doc_id) return get_document(doc) def post(self, request, id: str, doc_id: str): return self.get(request, id, doc_id) + def _user_has_permission(self, user): + return user.is_default_user() -class AbstractRemoveDocumentView(View): - model = None - document_model = None + def _user_has_shared_access(self, user, **kwargs): + obj = kwargs.get("id", None) + assert obj is not None + obj = get_object_or_404(self._MODEL_CLS, id=obj) + return obj.is_shared_with(user) + + +class AbstractRemoveDocumentView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = None + _DOCUMENT_CLS = None + _FORM_CLS = None + _MSG_SUCCESS = DOCUMENT_REMOVED_TEMPLATE class Meta: abstract = True - def get(self, request, id: str, doc_id: str): - """ Removes the document from the database and file system + def _get_redirect_url(self, *args, **kwargs): + return super()._get_redirect_url(*args, **kwargs) + "#related_data" - Wraps the generic functionality from konova.utils. + def _user_has_permission(self, user): + return user.is_default_user() - Args: - request (HttpRequest): The incoming request - id (str): The intervention id - doc_id (str): The document id - - Returns: - - """ - get_object_or_404(self.model, id=id) - doc = get_object_or_404(self.document_model, id=doc_id) - return remove_document( - request, - doc - ) - - def post(self, request, id: str, doc_id: str): - return self.get(request, id, doc_id) + def _get_msg_success(self, *args, **kwargs): + doc_id = kwargs.get("doc_id", None) + assert doc_id is not None + doc = get_object_or_404(self._DOCUMENT_CLS, id=doc_id) + return self._MSG_SUCCESS.format(doc.title) -class AbstractEditDocumentView(View): - model = None - document_model = None - form = None - redirect_url = None +class AbstractEditDocumentView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = None + _DOCUMENT_CLS = None + _FORM_CLS = EditDocumentModalForm + _REDIRECT_URL = None + _MSG_SUCCESS = DOCUMENT_EDITED class Meta: abstract = True - def get(self, request, id: str, doc_id: str): - """ GET handling for editing of existing document - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The intervention id - doc_id (str): The document id - - Returns: - - """ - obj = get_object_or_404(self.model, id=id) - doc = get_object_or_404(self.document_model, id=doc_id) - form = self.form(request.POST or None, request.FILES or None, instance=obj, document=doc, - request=request) - return form.process_request( - request, - DOCUMENT_EDITED, - redirect_url=reverse(self.redirect_url, args=(obj.id,)) + "#related_data" - ) - - def post(self, request, id: str, doc_id: str): - return self.get(request, id, doc_id) + def _user_has_permission(self, user): + return user.is_default_user() + def _get_redirect_url(self, *args, **kwargs): + return super()._get_redirect_url(*args, **kwargs) + "#related_data" \ No newline at end of file -- 2.47.2 From 1175fe3b37dfd4fc019ca33dba7a7a12ca6818f0 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 21 Oct 2025 14:06:11 +0200 Subject: [PATCH 26/36] # Parcel view refactoring * refactors parcel view to inherit from BaseView --- konova/views/geometry.py | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/konova/views/geometry.py b/konova/views/geometry.py index 30aa8cd9..087c5af3 100644 --- a/konova/views/geometry.py +++ b/konova/views/geometry.py @@ -10,15 +10,16 @@ from django.http import HttpResponse, HttpRequest from django.shortcuts import get_object_or_404 from django.template.loader import render_to_string from django.utils import timezone -from django.views import View from konova.models import Geometry from konova.settings import GEOM_THRESHOLD_RECALCULATION_SECONDS from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.tasks import celery_update_parcels +from konova.views.base import BaseView -class GeomParcelsView(View): +class GeomParcelsView(BaseView): + _TEMPLATE = "konova/includes/parcels/parcel_table_frame.html" def get(self, request: HttpRequest, id: str): """ Getter for HTMX @@ -32,7 +33,6 @@ class GeomParcelsView(View): Returns: A rendered piece of HTML """ - template = "konova/includes/parcels/parcel_table_frame.html" geom = get_object_or_404(Geometry, id=id) geos_geom = geom.geom or MultiPolygon(srid=DEFAULT_SRID_RLP) @@ -85,7 +85,7 @@ class GeomParcelsView(View): "geom_id": str(id), "next_page": next_page, } - html = render_to_string(template, context, request) + html = render_to_string(self._TEMPLATE, context, request) return HttpResponse(html, status=status_code) else: return HttpResponse(None, status=404) @@ -107,8 +107,15 @@ class GeomParcelsView(View): waiting_too_long = (pcs_diff >= wait_for_seconds) return waiting_too_long + def _user_has_shared_access(self, user, **kwargs): + return True -class GeomParcelsContentView(View): + def _user_has_permission(self, user): + return True + + +class GeomParcelsContentView(BaseView): + _TEMPLATE = "konova/includes/parcels/parcel_table_content.html" def get(self, request: HttpRequest, id: str, page: int): """ Getter for infinite scroll of HTMX @@ -130,7 +137,6 @@ class GeomParcelsContentView(View): # HTTP code 286 states that the HTMX should stop polling for updates # https://htmx.org/docs/#polling status_code = 286 - template = "konova/includes/parcels/parcel_table_content.html" geom = get_object_or_404(Geometry, id=id) parcels = geom.get_underlying_parcels() @@ -148,5 +154,11 @@ class GeomParcelsContentView(View): "geom_id": str(id), "next_page": next_page, } - html = render_to_string(template, context, request) + html = render_to_string(self._TEMPLATE, context, request) return HttpResponse(html, status=status_code) + + def _user_has_shared_access(self, user, **kwargs): + return True + + def _user_has_permission(self, user): + return True -- 2.47.2 From 1fc1b533cd1b2b9c5cbc69ff0c030cb6ce34c80f Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 21 Oct 2025 14:56:26 +0200 Subject: [PATCH 27/36] # Log view refactoring * refactors log views to inherit from BaseView --- compensation/views/compensation/log.py | 15 +++------------ compensation/views/eco_account/log.py | 15 +++------------ ema/views/log.py | 16 +++++----------- intervention/views/log.py | 14 +++----------- konova/views/log.py | 23 ++++++++++++++++------- 5 files changed, 30 insertions(+), 53 deletions(-) diff --git a/compensation/views/compensation/log.py b/compensation/views/compensation/log.py index 40b15a1e..78e0748b 100644 --- a/compensation/views/compensation/log.py +++ b/compensation/views/compensation/log.py @@ -5,20 +5,11 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator +from django.contrib.auth.mixins import LoginRequiredMixin from compensation.models import Compensation -from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.log import AbstractLogView -class CompensationLogView(AbstractLogView): - model = Compensation - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) +class CompensationLogView(LoginRequiredMixin, AbstractLogView): + _MODEL_CLS = Compensation diff --git a/compensation/views/eco_account/log.py b/compensation/views/eco_account/log.py index e18d945a..5c965f37 100644 --- a/compensation/views/eco_account/log.py +++ b/compensation/views/eco_account/log.py @@ -5,20 +5,11 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator +from django.contrib.auth.mixins import LoginRequiredMixin from compensation.models import EcoAccount -from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.log import AbstractLogView -class EcoAccountLogView(AbstractLogView): - model = EcoAccount - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) +class EcoAccountLogView(LoginRequiredMixin, AbstractLogView): + _MODEL_CLS = EcoAccount diff --git a/ema/views/log.py b/ema/views/log.py index 82162ba4..3f0ca939 100644 --- a/ema/views/log.py +++ b/ema/views/log.py @@ -5,20 +5,14 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator +from django.contrib.auth.mixins import LoginRequiredMixin from ema.models import Ema -from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.views.log import AbstractLogView -class EmaLogView(AbstractLogView): - model = Ema +class EmaLogView(LoginRequiredMixin, AbstractLogView): + _MODEL_CLS = Ema - @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) + def _user_has_permission(self, user): + return user.is_ets_user() diff --git a/intervention/views/log.py b/intervention/views/log.py index 709829c6..f06f6cb2 100644 --- a/intervention/views/log.py +++ b/intervention/views/log.py @@ -5,19 +5,11 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator +from django.contrib.auth.mixins import LoginRequiredMixin from intervention.models import Intervention -from konova.decorators import shared_access_required, default_group_required from konova.views.log import AbstractLogView -class InterventionLogView(AbstractLogView): - model = Intervention - - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Intervention, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) +class InterventionLogView(LoginRequiredMixin, AbstractLogView): + _MODEL_CLS = Intervention diff --git a/konova/views/log.py b/konova/views/log.py index 7cc9d56a..df898808 100644 --- a/konova/views/log.py +++ b/konova/views/log.py @@ -6,14 +6,15 @@ Created on: 19.08.22 """ from django.shortcuts import get_object_or_404, render -from django.views import View from django.utils.translation import gettext_lazy as _ from konova.contexts import BaseContext +from konova.views.base import BaseView -class AbstractLogView(View): - model = None +class AbstractLogView(BaseView): + _MODEL_CLS = None + _TEMPLATE = "modal/modal_generic.html" class Meta: abstract = True @@ -28,14 +29,22 @@ class AbstractLogView(View): Returns: """ - intervention = get_object_or_404(self.model, id=id) - template = "modal/modal_generic.html" + intervention = get_object_or_404(self._MODEL_CLS, id=id) body_template = "log.html" context = { "modal_body_template": body_template, - "log": intervention.log.all(), + "log": intervention.log.iterator(), "modal_title": _("Log"), } context = BaseContext(request, context).context - return render(request, template, context) + return render(request, self._TEMPLATE, context) + + def _user_has_shared_access(self, user, **kwargs): + obj_id = kwargs.get('id', None) + assert obj_id is not None + obj = get_object_or_404(self._MODEL_CLS, id=obj_id) + return obj.is_shared_with(user) + + def _user_has_permission(self, user): + return user.is_default_user() -- 2.47.2 From 3a9c4e13f688ff563cd909655ded3f32c99fe716 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 21 Oct 2025 17:03:12 +0200 Subject: [PATCH 28/36] # Resubmission view refactoring * refactors resubmission view for eiv, kom, oek, ema * removes unused attributes on BaseModalFormView --- compensation/forms/modals/resubmission.py | 15 ++++++ .../views/compensation/resubmission.py | 18 ++----- .../views/eco_account/resubmission.py | 18 ++----- ema/forms.py | 8 ++- ema/views/resubmission.py | 20 +++----- intervention/forms/modals/resubmission.py | 11 ++++ intervention/views/check.py | 2 - intervention/views/resubmission.py | 18 ++----- konova/forms/base_form.py | 1 - konova/utils/message_templates.py | 5 +- konova/views/base.py | 8 +-- konova/views/resubmission.py | 51 ++++--------------- 12 files changed, 69 insertions(+), 106 deletions(-) create mode 100644 compensation/forms/modals/resubmission.py create mode 100644 intervention/forms/modals/resubmission.py diff --git a/compensation/forms/modals/resubmission.py b/compensation/forms/modals/resubmission.py new file mode 100644 index 00000000..a723602a --- /dev/null +++ b/compensation/forms/modals/resubmission.py @@ -0,0 +1,15 @@ +""" +Author: Michel Peltriaux +Created on: 21.10.25 + +""" +from compensation.models import Compensation, EcoAccount +from konova.forms.modals import ResubmissionModalForm + + +class CompensationResubmissionModalForm(ResubmissionModalForm): + _MODEL_CLS = Compensation + + +class EcoAccountResubmissionModalForm(ResubmissionModalForm): + _MODEL_CLS = EcoAccount diff --git a/compensation/views/compensation/resubmission.py b/compensation/views/compensation/resubmission.py index 31b073e7..15f9b9d1 100644 --- a/compensation/views/compensation/resubmission.py +++ b/compensation/views/compensation/resubmission.py @@ -5,22 +5,12 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - +from compensation.forms.modals.resubmission import CompensationResubmissionModalForm from compensation.models import Compensation -from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.resubmission import AbstractResubmissionView class CompensationResubmissionView(AbstractResubmissionView): - model = Compensation - redirect_url_base = "compensation:detail" - form_action_url_base = "compensation:resubmission-create" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _FORM_CLS = CompensationResubmissionModalForm + _REDIRECT_URL = "compensation:detail" diff --git a/compensation/views/eco_account/resubmission.py b/compensation/views/eco_account/resubmission.py index 19b8dca4..bf66488a 100644 --- a/compensation/views/eco_account/resubmission.py +++ b/compensation/views/eco_account/resubmission.py @@ -5,22 +5,12 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - +from compensation.forms.modals.resubmission import EcoAccountResubmissionModalForm from compensation.models import EcoAccount -from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.resubmission import AbstractResubmissionView class EcoAccountResubmissionView(AbstractResubmissionView): - model = EcoAccount - redirect_url_base = "compensation:acc:detail" - form_action_url_base = "compensation:acc:resubmission-create" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _FORM_CLS = EcoAccountResubmissionModalForm + _REDIRECT_URL = "compensation:acc:detail" diff --git a/ema/forms.py b/ema/forms.py index 5a98a903..8b1a0c9d 100644 --- a/ema/forms.py +++ b/ema/forms.py @@ -15,7 +15,8 @@ from compensation.forms.compensation import AbstractCompensationForm from ema.models import Ema, EmaDocument from intervention.models import Responsibility, Handler from konova.forms import SimpleGeomForm -from konova.forms.modals import NewDocumentModalForm, EditDocumentModalForm, RemoveDocumentModalForm +from konova.forms.modals import NewDocumentModalForm, EditDocumentModalForm, RemoveDocumentModalForm, \ + ResubmissionModalForm from user.models import UserActionLogEntry @@ -176,4 +177,7 @@ class EditEmaDocumentModalForm(EditDocumentModalForm): _DOCUMENT_CLS = EmaDocument class RemoveEmaDocumentModalForm(RemoveDocumentModalForm): - _DOCUMENT_CLS = EmaDocument \ No newline at end of file + _DOCUMENT_CLS = EmaDocument + +class EmaResubmissionModalForm(ResubmissionModalForm): + _MODEL_CLS = Ema diff --git a/ema/views/resubmission.py b/ema/views/resubmission.py index c07c79ee..76ea60bd 100644 --- a/ema/views/resubmission.py +++ b/ema/views/resubmission.py @@ -5,22 +5,16 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - +from ema.forms import EmaResubmissionModalForm from ema.models import Ema -from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.views.resubmission import AbstractResubmissionView class EmaResubmissionView(AbstractResubmissionView): - model = Ema - redirect_url_base = "ema:detail" - form_action_url_base = "ema:resubmission-create" + _MODEL_CLS = Ema + _FORM_CLS = EmaResubmissionModalForm + _REDIRECT_URL = "ema:detail" + action_url = "ema:resubmission-create" - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(conservation_office_group_required) - @method_decorator(shared_access_required(Ema, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + def _user_has_permission(self, user): + return user.is_ets_user() diff --git a/intervention/forms/modals/resubmission.py b/intervention/forms/modals/resubmission.py new file mode 100644 index 00000000..04b9ef5d --- /dev/null +++ b/intervention/forms/modals/resubmission.py @@ -0,0 +1,11 @@ +""" +Author: Michel Peltriaux +Created on: 21.10.25 + +""" +from intervention.models import Intervention +from konova.forms.modals import ResubmissionModalForm + + +class InterventionResubmissionModalForm(ResubmissionModalForm): + _MODEL_CLS = Intervention diff --git a/intervention/views/check.py b/intervention/views/check.py index 3ed0cdfb..07387913 100644 --- a/intervention/views/check.py +++ b/intervention/views/check.py @@ -10,7 +10,6 @@ from django.utils.translation import gettext_lazy as _ from intervention.forms.modals.check import CheckModalForm from intervention.models import Intervention -from konova.utils.message_templates import INTERVENTION_INVALID from konova.views.base import BaseModalFormView @@ -18,7 +17,6 @@ class InterventionCheckView(LoginRequiredMixin, BaseModalFormView): _MODEL_CLS = Intervention _FORM_CLS = CheckModalForm _MSG_SUCCESS = _("Check performed") - _MSG_ERROR = INTERVENTION_INVALID _REDIRECT_URL = "intervention:detail" def _user_has_permission(self, user): diff --git a/intervention/views/resubmission.py b/intervention/views/resubmission.py index 37fbd632..e3a3a053 100644 --- a/intervention/views/resubmission.py +++ b/intervention/views/resubmission.py @@ -5,22 +5,12 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - +from intervention.forms.modals.resubmission import InterventionResubmissionModalForm from intervention.models import Intervention -from konova.decorators import default_group_required, shared_access_required, login_required_modal from konova.views.resubmission import AbstractResubmissionView class InterventionResubmissionView(AbstractResubmissionView): - model = Intervention - redirect_url_base = "intervention:detail" - form_action_url_base = "intervention:resubmission-create" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Intervention, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Intervention + _FORM_CLS = InterventionResubmissionModalForm + _REDIRECT_URL = "intervention:detail" diff --git a/konova/forms/base_form.py b/konova/forms/base_form.py index ba526396..e648f796 100644 --- a/konova/forms/base_form.py +++ b/konova/forms/base_form.py @@ -10,7 +10,6 @@ from abc import abstractmethod from django import forms from django.utils.translation import gettext_lazy as _ -from compensation.models import EcoAccount from konova.models import BaseObject diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py index 09ead4da..860dc9eb 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -98,4 +98,7 @@ DATA_CHECKED_PREVIOUSLY_TEMPLATE = _("Data has changed since last check on {} by DATA_IS_UNCHECKED = _("Current data not checked yet") # API TOKEN SETTINGS -NEW_API_TOKEN_GENERATED = _("New token generated. Administrators need to validate.") \ No newline at end of file +NEW_API_TOKEN_GENERATED = _("New token generated. Administrators need to validate.") + +# RESUBMISSION +NEW_RESUBMISSION_CREATED = _("Resubmission set") diff --git a/konova/views/base.py b/konova/views/base.py index 3c5d8499..a1de7e29 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -44,6 +44,7 @@ class BaseView(View): return super().dispatch(request, *args, **kwargs) + @abstractmethod def _user_has_permission(self, user): """ Has to be implemented properly by inheriting classes @@ -53,8 +54,9 @@ class BaseView(View): Returns: """ - return False + raise NotImplementedError("User permission not checked!") + @abstractmethod def _user_has_shared_access(self, user, **kwargs): """ Has to be implemented properly by inheriting classes @@ -64,7 +66,7 @@ class BaseView(View): Returns: """ - return False + raise NotImplementedError("Shared access not checked!") def _get_redirect_url(self, *args, **kwargs): return self._REDIRECT_URL @@ -76,9 +78,7 @@ class BaseModalFormView(BaseView): _TEMPLATE = "modal/modal_form.html" _MODEL_CLS = None _FORM_CLS = None - _TAB_TITLE = None _MSG_SUCCESS = None - _MSG_ERROR = None class Meta: abstract = True diff --git a/konova/views/resubmission.py b/konova/views/resubmission.py index 634949de..cbe21af0 100644 --- a/konova/views/resubmission.py +++ b/konova/views/resubmission.py @@ -5,51 +5,20 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.shortcuts import get_object_or_404 -from django.urls import reverse -from django.views import View -from django.utils.translation import gettext_lazy as _ +from django.contrib.auth.mixins import LoginRequiredMixin -from konova.forms.modals import ResubmissionModalForm +from konova.utils.message_templates import NEW_RESUBMISSION_CREATED +from konova.views.base import BaseModalFormView -class AbstractResubmissionView(View): - model = None - form_action_url_base = None - redirect_url_base = None +class AbstractResubmissionView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = None + _FORM_CLS = None + _REDIRECT_URL = None + _MSG_SUCCESS = NEW_RESUBMISSION_CREATED class Meta: abstract = True - - def get(self, request, id: str): - """ Renders resubmission form for an object - Args: - request (HttpRequest): The incoming request - id (str): Object's id - - Returns: - - """ - obj = get_object_or_404(self.model, id=id) - form = ResubmissionModalForm(request.POST or None, instance=obj, request=request) - form.action_url = reverse(self.form_action_url_base, args=(id,)) - return form.process_request( - request, - msg_success=_("Resubmission set"), - redirect_url=reverse(self.redirect_url_base, args=(id,)) - ) - - def post(self, request, id: str): - """ - - BaseModalForm provides the method process_request() which handles GET as well as POST requests. It was written - for easier handling of function based views. To support process_request() on class based views, the post() - call needs to be treated the same way as the get() call. - - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id - - """ - return self.get(request, id) + def _user_has_permission(self, user): + return user.is_default_user() -- 2.47.2 From 554ade6794e7f583b4fc268afdfaef5b6a639f0b Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 21 Oct 2025 19:15:17 +0200 Subject: [PATCH 29/36] # Share view refactoring * refactors share views for eiv, oek, ema (kom does not have any) --- compensation/views/eco_account/share.py | 22 ++--------- ema/views/share.py | 24 +++--------- intervention/views/share.py | 22 ++--------- konova/views/share.py | 50 +++++++++++-------------- 4 files changed, 35 insertions(+), 83 deletions(-) diff --git a/compensation/views/eco_account/share.py b/compensation/views/eco_account/share.py index 19c8903a..3dade1e6 100644 --- a/compensation/views/eco_account/share.py +++ b/compensation/views/eco_account/share.py @@ -5,29 +5,15 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - from compensation.models import EcoAccount -from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.share import AbstractShareByTokenView, AbstractShareFormView class EcoAccountShareByTokenView(AbstractShareByTokenView): - model = EcoAccount - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _REDIRECT_URL = "compensation:acc:detail" class EcoAccountShareFormView(AbstractShareFormView): - model = EcoAccount - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _REDIRECT_URL = "compensation:acc:detail" diff --git a/ema/views/share.py b/ema/views/share.py index 00b75e7c..84ebfdbf 100644 --- a/ema/views/share.py +++ b/ema/views/share.py @@ -5,29 +5,17 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - from ema.models import Ema -from konova.decorators import conservation_office_group_required, shared_access_required, login_required_modal from konova.views.share import AbstractShareByTokenView, AbstractShareFormView class EmaShareByTokenView(AbstractShareByTokenView): - model = Ema - redirect_url = "ema:detail" - - @method_decorator(login_required) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) - + _MODEL_CLS = Ema + _REDIRECT_URL = "ema:detail" class EmaShareFormView(AbstractShareFormView): - model = Ema + _MODEL_CLS = Ema + _REDIRECT_URL = "ema:detail" - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(conservation_office_group_required) - @method_decorator(shared_access_required(Ema, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + def _user_has_permission(self, user): + return user.is_ets_user() \ No newline at end of file diff --git a/intervention/views/share.py b/intervention/views/share.py index c72e2183..deee54e6 100644 --- a/intervention/views/share.py +++ b/intervention/views/share.py @@ -5,29 +5,15 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - from intervention.models import Intervention -from konova.decorators import default_group_required, shared_access_required, login_required_modal from konova.views.share import AbstractShareByTokenView, AbstractShareFormView class InterventionShareByTokenView(AbstractShareByTokenView): - model = Intervention - redirect_url = "intervention:detail" - - @method_decorator(login_required) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Intervention + _REDIRECT_URL = "intervention:detail" class InterventionShareFormView(AbstractShareFormView): - model = Intervention - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Intervention, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Intervention + _REDIRECT_URL = "intervention:detail" \ No newline at end of file diff --git a/konova/views/share.py b/konova/views/share.py index abcbecaa..4d894338 100644 --- a/konova/views/share.py +++ b/konova/views/share.py @@ -6,24 +6,24 @@ Created on: 22.08.22 """ from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import get_object_or_404, redirect -from django.views import View from django.utils.translation import gettext_lazy as _ from intervention.forms.modals.share import ShareModalForm from konova.utils.message_templates import DATA_SHARE_SET +from konova.views.base import BaseView, BaseModalFormView -class AbstractShareByTokenView(View): - model = None - redirect_url = None +class AbstractShareByTokenView(LoginRequiredMixin, BaseView): + _MODEL_CLS = None + _REDIRECT_URL = None class Meta: abstract = True def get(self, request, id: str, token: str): - - """ Performs sharing of an intervention + """ Performs sharing of an entry If token given in url is not valid, the user will be redirected to the dashboard @@ -36,7 +36,7 @@ class AbstractShareByTokenView(View): """ user = request.user - obj = get_object_or_404(self.model, id=id) + obj = get_object_or_404(self._MODEL_CLS, id=id) # Check tokens if obj.access_token == token: # Send different messages in case user has already been added to list of sharing users @@ -51,7 +51,7 @@ class AbstractShareByTokenView(View): _("{} has been shared with you").format(obj.identifier) ) obj.share_with_user(user) - return redirect(self.redirect_url, id=id) + return redirect(self._REDIRECT_URL, id=id) else: messages.error( request, @@ -60,29 +60,21 @@ class AbstractShareByTokenView(View): ) return redirect("home") + def _user_has_permission(self, user): + return user.is_default_user() -class AbstractShareFormView(View): - model = None + def _user_has_shared_access(self, user, **kwargs): + # The user does not need to have shared access to call the endpoint which gives them shared access + return True + + +class AbstractShareFormView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = None + _FORM_CLS = ShareModalForm + _MSG_SUCCESS = DATA_SHARE_SET class Meta: abstract = True - - def get(self, request, id: str): - """ Renders sharing form - Args: - request (HttpRequest): The incoming request - id (str): Object's id - - Returns: - - """ - obj = get_object_or_404(self.model, id=id) - form = ShareModalForm(request.POST or None, instance=obj, request=request) - return form.process_request( - request, - msg_success=DATA_SHARE_SET - ) - - def post(self, request, id: str): - return self.get(request, id) + def _user_has_permission(self, user): + return user.is_default_user() -- 2.47.2 From fe2ac3d97d1ec6fa11d351714e1e3c36f8b5d8f1 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 21 Oct 2025 19:37:34 +0200 Subject: [PATCH 30/36] # Test update * fixes bug for sharing via token where permission was too tight --- konova/tests/test_views.py | 2 +- konova/views/share.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index 4977bfee..4d95fbd3 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -543,7 +543,7 @@ class BaseViewTestCase(BaseTestCase): for url, redirect_to in urls.items(): response = client.get(url, follow=True) # Expect redirects to the landing page - self.assertEqual(response.redirect_chain[0], (redirect_to, 302), msg=f"Failed for {url}") + self.assertEqual(response.redirect_chain[0], (redirect_to, 302), msg=f"Failed for {url}. Expected {redirect_to}") def assert_url_fail(self, client: Client, urls: list): """ Assert for all given urls a direct 302 response diff --git a/konova/views/share.py b/konova/views/share.py index 4d894338..482e62f7 100644 --- a/konova/views/share.py +++ b/konova/views/share.py @@ -61,7 +61,8 @@ class AbstractShareByTokenView(LoginRequiredMixin, BaseView): return redirect("home") def _user_has_permission(self, user): - return user.is_default_user() + # No permissions are needed to get shared access via token + return True def _user_has_shared_access(self, user, **kwargs): # The user does not need to have shared access to call the endpoint which gives them shared access -- 2.47.2 From 837c1de938246b6a283dcfa466d55a901697ea8d Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 21 Oct 2025 20:28:43 +0200 Subject: [PATCH 31/36] # Remove View refactoring * refactors remove view for kom, eiv, oek and ema * introduces BaseRemoveModalFormView * moves html blocking logic from BaseModalForm into BaseModalFormView --- compensation/forms/modals/state.py | 2 +- compensation/urls/compensation.py | 6 +-- compensation/urls/eco_account.py | 7 ++-- .../views/compensation/compensation.py | 28 ++++---------- compensation/views/eco_account/deduction.py | 4 ++ compensation/views/eco_account/eco_account.py | 37 ++++--------------- ema/urls.py | 6 +-- ema/views/ema.py | 33 +++-------------- intervention/urls.py | 7 ++-- intervention/views/intervention.py | 31 +++------------- konova/forms/base_form.py | 35 ------------------ konova/forms/modals/base_form.py | 4 +- konova/utils/message_templates.py | 3 ++ konova/views/base.py | 35 +++++++++++++++++- konova/views/record.py | 4 ++ konova/views/remove.py | 28 ++++++++++++++ konova/views/resubmission.py | 4 ++ 17 files changed, 117 insertions(+), 157 deletions(-) create mode 100644 konova/views/remove.py diff --git a/compensation/forms/modals/state.py b/compensation/forms/modals/state.py index 7340c95f..3ff054d6 100644 --- a/compensation/forms/modals/state.py +++ b/compensation/forms/modals/state.py @@ -106,7 +106,7 @@ class NewCompensationStateModalForm(BaseModalForm): """ redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home") - template = self.template + template = self._TEMPLATE if request.method == "POST": if self.is_valid(): # Modal forms send one POST for checking on data validity. This can be used to return possible errors diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index b410ac78..d97565f2 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -18,8 +18,8 @@ from compensation.views.compensation.action import NewCompensationActionView, Ed from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \ RemoveCompensationStateView from compensation.views.compensation.compensation import \ - remove_view, CompensationIndexView, CompensationIdentifierGeneratorView, CompensationDetailView, \ - NewCompensationFormView, EditCompensationFormView + CompensationIndexView, CompensationIdentifierGeneratorView, CompensationDetailView, \ + NewCompensationFormView, EditCompensationFormView, RemoveCompensationView from compensation.views.compensation.log import CompensationLogView urlpatterns = [ @@ -31,7 +31,7 @@ urlpatterns = [ path('', CompensationDetailView.as_view(), name='detail'), path('/log', CompensationLogView.as_view(), name='log'), path('/edit', EditCompensationFormView.as_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 fc3c6116..d3d143f1 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -8,9 +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 remove_view, \ - EcoAccountIndexView, EcoAccountIdentifierGeneratorView, EcoAccountDetailView, NewEcoAccountFormView, \ - EditEcoAccountFormView +from compensation.views.eco_account.eco_account import EcoAccountIndexView, EcoAccountIdentifierGeneratorView, \ + EcoAccountDetailView, NewEcoAccountFormView, EditEcoAccountFormView, RemoveEcoAccountView from compensation.views.eco_account.log import EcoAccountLogView from compensation.views.eco_account.record import EcoAccountRecordView from compensation.views.eco_account.report import EcoAccountReportView @@ -37,7 +36,7 @@ urlpatterns = [ path('/record', EcoAccountRecordView.as_view(), name='record'), path('/report', EcoAccountReportView.as_view(), name='report'), path('/edit', EditEcoAccountFormView.as_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 35ff6510..098849d8 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -25,6 +25,7 @@ from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_C from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ BaseEditSpatialLocatedObjectFormView from konova.views.detail import BaseDetailView +from konova.views.remove import BaseRemoveModalFormView class CompensationIndexView(LoginRequiredMixin, BaseIndexView): @@ -164,25 +165,10 @@ class CompensationDetailView(BaseDetailView): return 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"), - ) +class RemoveCompensationView(LoginRequiredMixin, BaseRemoveModalFormView): + _MODEL_CLS = Compensation + _FORM_CLS = RemoveModalForm + _REDIRECT_URL = "compensation:index" + def _user_has_permission(self, user): + return user.is_default_user() diff --git a/compensation/views/eco_account/deduction.py b/compensation/views/eco_account/deduction.py index 01e84f3f..0dddc1eb 100644 --- a/compensation/views/eco_account/deduction.py +++ b/compensation/views/eco_account/deduction.py @@ -22,6 +22,10 @@ class NewEcoAccountDeductionView(LoginRequiredMixin, AbstractNewDeductionView): if not obj.recorded: raise Http404() + def _check_for_recorded_instance(self, obj): + # Deductions can be created on recorded as well as on non-recorded entries + return None + class EditEcoAccountDeductionView(LoginRequiredMixin, AbstractEditDeductionView): _MODEL_CLS = EcoAccount diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 95dd8005..f9c50ab8 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -26,6 +26,7 @@ from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECO from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ BaseEditSpatialLocatedObjectFormView from konova.views.detail import BaseDetailView +from konova.views.remove import BaseRemoveModalFormView class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView): @@ -254,34 +255,10 @@ class EcoAccountDetailView(BaseDetailView): return 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"), - ) +class RemoveEcoAccountView(LoginRequiredMixin, BaseRemoveModalFormView): + _MODEL_CLS = EcoAccount + _FORM_CLS = RemoveEcoAccountModalForm + _REDIRECT_URL = "compensation:acc:index" + def _user_has_permission(self, user): + return user.is_default_user() diff --git a/ema/urls.py b/ema/urls.py index bfc6c0f4..835530bb 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -10,8 +10,8 @@ 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 remove_view, EmaIndexView, \ - EmaIdentifierGeneratorView, EmaDetailView, EditEmaFormView, NewEmaFormView +from ema.views.ema import EmaIndexView, EmaIdentifierGeneratorView, EmaDetailView, EditEmaFormView, NewEmaFormView, \ + RemoveEmaView from ema.views.log import EmaLogView from ema.views.record import EmaRecordView from ema.views.report import EmaReportView @@ -27,7 +27,7 @@ urlpatterns = [ path("", EmaDetailView.as_view(), name="detail"), path('/log', EmaLogView.as_view(), name='log'), path('/edit', EditEmaFormView.as_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', EmaReportView.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 d454ffba..6057a95c 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -5,21 +5,17 @@ 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.http import HttpRequest from django.shortcuts import get_object_or_404 -from django.urls import reverse from django.utils.translation import gettext_lazy as _ from ema.forms import NewEmaForm, EditEmaForm from ema.models import Ema from ema.tables import EmaTable -from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal -from konova.forms.modals import RemoveModalForm from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ BaseEditSpatialLocatedObjectFormView from konova.views.detail import BaseDetailView +from konova.views.remove import BaseRemoveModalFormView class EmaIndexView(LoginRequiredMixin, BaseIndexView): @@ -112,26 +108,9 @@ class EmaDetailView(BaseDetailView): } return context +class RemoveEmaView(LoginRequiredMixin, BaseRemoveModalFormView): + _MODEL_CLS = Ema + _REDIRECT_URL = "ema:index" -@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"), - ) - + def _user_has_permission(self, user): + return user.is_ets_user() diff --git a/intervention/urls.py b/intervention/urls.py index e179d000..39024f79 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -14,9 +14,8 @@ from intervention.views.deduction import NewInterventionDeductionView, EditInter RemoveInterventionDeductionView from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \ RemoveInterventionDocumentView, EditInterventionDocumentView -from intervention.views.intervention import remove_view, \ - InterventionIndexView, InterventionIdentifierGeneratorView, InterventionDetailView, NewInterventionFormView, \ - EditInterventionFormView +from intervention.views.intervention import InterventionIndexView, InterventionIdentifierGeneratorView, \ + InterventionDetailView, NewInterventionFormView, EditInterventionFormView, RemoveInterventionView from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView from intervention.views.report import InterventionReportView @@ -33,7 +32,7 @@ urlpatterns = [ path('', InterventionDetailView.as_view(), name='detail'), path('/log', InterventionLogView.as_view(), name='log'), path('/edit', EditInterventionFormView.as_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', InterventionCheckView.as_view(), name='check'), diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index cd0e9308..1350340a 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -10,22 +10,21 @@ from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest from django.shortcuts import get_object_or_404, render, redirect -from django.urls import reverse from django.utils.translation import gettext_lazy as _ from intervention.forms.intervention import EditInterventionForm, NewInterventionForm from intervention.models import Intervention from intervention.tables import InterventionTable from konova.contexts import BaseContext -from konova.decorators import default_group_required, shared_access_required, 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 DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ CHECK_STATE_RESET, FORM_INVALID, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ BaseEditSpatialLocatedObjectFormView from konova.views.detail import BaseDetailView +from konova.views.remove import BaseRemoveModalFormView class InterventionIndexView(LoginRequiredMixin, BaseIndexView): @@ -181,26 +180,6 @@ 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") - ) +class RemoveInterventionView(LoginRequiredMixin, BaseRemoveModalFormView): + _MODEL_CLS = Intervention + _REDIRECT_URL = "intervention:index" diff --git a/konova/forms/base_form.py b/konova/forms/base_form.py index e648f796..2e1cc960 100644 --- a/konova/forms/base_form.py +++ b/konova/forms/base_form.py @@ -10,14 +10,11 @@ from abc import abstractmethod from django import forms from django.utils.translation import gettext_lazy as _ -from konova.models import BaseObject - class BaseForm(forms.Form): """ Basic form for that holds attributes needed in all other forms """ - template = None action_url = None action_btn_label = _("Save") form_title = None @@ -43,7 +40,6 @@ class BaseForm(forms.Form): self.has_required_fields = True break - self.check_for_recorded_instance() self.__check_valid_label_input_ratio() @abstractmethod @@ -137,34 +133,3 @@ class BaseForm(forms.Form): set_class = self.fields[field].widget.attrs.get("class", "") set_class = set_class.replace(cls, "") self.fields[field].widget.attrs["class"] = set_class - - def check_for_recorded_instance(self): - """ Checks if the instance is recorded and runs some special logic if yes - - If the instance is recorded, the form shall not display any possibility to - edit any data. Instead, the users should get some information about why they can not edit anything. - - There are situations where the form should be rendered regularly, - e.g deduction forms for (recorded) eco accounts. - - Returns: - - """ - is_none = self.instance is None - is_other_data_type = not isinstance(self.instance, BaseObject) - - if is_none or is_other_data_type: - # Do nothing - return - - if self.instance.is_recorded: - self.block_form() - - def block_form(self): - """ - Overwrites template, providing no actions - - Returns: - - """ - self.template = "form/recorded_no_edit.html" \ No newline at end of file diff --git a/konova/forms/modals/base_form.py b/konova/forms/modals/base_form.py index 96539cf9..93127b0e 100644 --- a/konova/forms/modals/base_form.py +++ b/konova/forms/modals/base_form.py @@ -23,7 +23,7 @@ class BaseModalForm(BaseForm, BSModalForm): """ is_modal_form = True render_submit = True - template = "modal/modal_form.html" + _TEMPLATE = "modal/modal_form.html" def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) @@ -43,7 +43,7 @@ class BaseModalForm(BaseForm, BSModalForm): """ redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home") - template = self.template + template = self._TEMPLATE if request.method == "POST": if self.is_valid(): if not is_ajax(request.META): diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py index 860dc9eb..a7e05c53 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -20,6 +20,9 @@ ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office us MISSING_GROUP_PERMISSION = _("You need to be part of another user group.") CHECK_STATE_RESET = _("Status of Checked reset") +# REMOVED +GENERIC_REMOVED_TEMPLATE = _("{} removed") + # RECORDING RECORDED_BLOCKS_EDIT = _("Entry is recorded. To edit data, the entry first needs to be unrecorded.") ENTRY_RECORDED = _("{} recorded") diff --git a/konova/views/base.py b/konova/views/base.py index a1de7e29..5f9db037 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -16,8 +16,9 @@ from django.utils.translation import gettext_lazy as _ from konova.contexts import BaseContext from konova.forms import BaseForm, SimpleGeomForm +from konova.models import BaseObject from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.general import check_user_is_in_any_group, check_id_is_valid_uuid +from konova.utils.general import check_user_is_in_any_group from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED, IDENTIFIER_REPLACED, \ GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, RECORDED_BLOCKS_EDIT, FORM_INVALID @@ -89,6 +90,7 @@ class BaseModalFormView(BaseView): def get(self, request: HttpRequest, id: str, *args, **kwargs): obj = self._MODEL_CLS.objects.get(id=id) + self._check_for_recorded_instance(obj) form = self._FORM_CLS( request.POST or None, request.FILES or None, @@ -104,6 +106,7 @@ class BaseModalFormView(BaseView): def post(self, request: HttpRequest, id: str, *args, **kwargs): obj = self._MODEL_CLS.objects.get(id=id) + self._check_for_recorded_instance(obj) form = self._FORM_CLS( request.POST or None, request.FILES or None, @@ -140,6 +143,36 @@ class BaseModalFormView(BaseView): def _get_msg_success(self, *args, **kwargs): return self._MSG_SUCCESS + def _check_for_recorded_instance(self, obj): + """ Checks if the object on this view is recorded and runs some special logic if yes + + If the instance is recorded, the view should provide some information about why the user can not edit anything. + + There are situations where the form should be rendered regularly, + e.g deduction forms for (recorded) eco accounts. + + Returns: + + """ + is_none = obj is None + is_other_data_type = not isinstance(obj, BaseObject) + + if is_none or is_other_data_type: + # Do nothing + return + + if obj.is_recorded: + self._block_form() + + def _block_form(self): + """ + Overwrites template, providing no actions + + Returns: + + """ + self._TEMPLATE = "form/recorded_no_edit.html" + class BaseIndexView(BaseView): """ Base class for index views diff --git a/konova/views/record.py b/konova/views/record.py index 7f3c41ce..cf780acb 100644 --- a/konova/views/record.py +++ b/konova/views/record.py @@ -26,3 +26,7 @@ class AbstractRecordView(BaseModalFormView): return ENTRY_RECORDED.format(obj.identifier) else: return ENTRY_UNRECORDED.format(obj.identifier) + + def _check_for_recorded_instance(self, obj): + # Do not block record view if instance might be recorded + return None \ No newline at end of file diff --git a/konova/views/remove.py b/konova/views/remove.py new file mode 100644 index 00000000..82b1e57a --- /dev/null +++ b/konova/views/remove.py @@ -0,0 +1,28 @@ +""" +Author: Michel Peltriaux +Created on: 21.10.25 + +""" +from django.urls import reverse + +from konova.forms.modals import RemoveModalForm +from konova.utils.message_templates import GENERIC_REMOVED_TEMPLATE +from konova.views.base import BaseModalFormView + + +class BaseRemoveModalFormView(BaseModalFormView): + _MODEL_CLS = None + _FORM_CLS = RemoveModalForm + _MSG_SUCCESS = GENERIC_REMOVED_TEMPLATE + _REDIRECT_URL = None + + def _user_has_permission(self, user): + return user.is_default_user() + + def _get_redirect_url(self, *args, **kwargs): + return reverse(self._REDIRECT_URL) + + def _get_msg_success(self, *args, **kwargs): + obj = kwargs.get("obj", None) + assert obj is not None + return self._MSG_SUCCESS.format(obj.identifier) diff --git a/konova/views/resubmission.py b/konova/views/resubmission.py index cbe21af0..940d09f7 100644 --- a/konova/views/resubmission.py +++ b/konova/views/resubmission.py @@ -22,3 +22,7 @@ class AbstractResubmissionView(LoginRequiredMixin, BaseModalFormView): def _user_has_permission(self, user): return user.is_default_user() + + def _check_for_recorded_instance(self, obj): + # Resubmissions are allowed despite an entry being recorded + return None \ No newline at end of file -- 2.47.2 From d85ebccec854e1853f1451b0aad2238f95768722 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 21 Oct 2025 21:05:45 +0200 Subject: [PATCH 32/36] # Compensation State view refactoring * refactors compensation state views for kom, ema, oek * updates tests * refactors before-after state toggling into initialization of NewCompensationStateModalForm --- compensation/forms/modals/state.py | 71 +++---------- .../tests/compensation/unit/test_forms.py | 46 ++++++-- compensation/views/compensation/state.py | 37 ++----- compensation/views/eco_account/state.py | 37 ++----- ema/views/state.py | 40 +++---- konova/views/state.py | 100 +++++------------- 6 files changed, 96 insertions(+), 235 deletions(-) diff --git a/compensation/forms/modals/state.py b/compensation/forms/modals/state.py index 3ff054d6..4a11a3ad 100644 --- a/compensation/forms/modals/state.py +++ b/compensation/forms/modals/state.py @@ -5,21 +5,17 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 18.08.22 """ -from bootstrap_modal_forms.mixins import is_ajax from dal import autocomplete from django import forms -from django.contrib import messages -from django.http import HttpResponseRedirect, HttpRequest -from django.shortcuts import render from django.utils.translation import gettext_lazy as _ from codelist.models import KonovaCode from codelist.settings import CODELIST_BIOTOPES_ID, \ CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID +from compensation.models import CompensationState from intervention.inputs import CompensationStateTreeRadioSelect -from konova.contexts import BaseContext from konova.forms.modals import RemoveModalForm, BaseModalForm -from konova.utils.message_templates import COMPENSATION_STATE_EDITED, FORM_INVALID, ADDED_COMPENSATION_STATE +from konova.utils.message_templates import COMPENSATION_STATE_EDITED, ADDED_COMPENSATION_STATE class NewCompensationStateModalForm(BaseModalForm): @@ -68,10 +64,13 @@ class NewCompensationStateModalForm(BaseModalForm): ) ) + _is_before_state: bool = False + def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.form_title = _("New state") self.form_caption = _("Insert data for the new state") + self._is_before_state = bool(self.request.GET.get("before", False)) choices = KonovaCode.objects.filter( code_lists__in=[CODELIST_BIOTOPES_ID], is_archived=False, @@ -83,65 +82,19 @@ class NewCompensationStateModalForm(BaseModalForm): ] self.fields["biotope_type"].choices = choices - def save(self, is_before_state: bool = False): - state = self.instance.add_state(self, is_before_state) + def save(self): + state = self.instance.add_state(self, self._is_before_state) self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_STATE) return state - def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None): - """ Generic processing of request - - Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used - - +++ - The generic method from super class can not be used, since we need to do some request parameter check in here. - +++ - - Args: - request (HttpRequest): The incoming request - msg_success (str): The message in case of successful removing - msg_error (str): The message in case of an error - - Returns: - - """ - redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home") - template = self._TEMPLATE - if request.method == "POST": - if self.is_valid(): - # Modal forms send one POST for checking on data validity. This can be used to return possible errors - # on the form. A second POST (if no errors occured) is sent afterwards and needs to process the - # saving/commiting of the data to the database. is_ajax() performs this check. The first request is - # an ajax call, the second is a regular form POST. - if not is_ajax(request.META): - is_before_state = bool(request.GET.get("before", False)) - self.save(is_before_state=is_before_state) - messages.success( - request, - msg_success - ) - return HttpResponseRedirect(redirect_url) - else: - context = { - "form": self, - } - context = BaseContext(request, context).context - return render(request, template, context) - elif request.method == "GET": - context = { - "form": self, - } - context = BaseContext(request, context).context - return render(request, template, context) - else: - raise NotImplementedError - class EditCompensationStateModalForm(NewCompensationStateModalForm): state = None def __init__(self, *args, **kwargs): - self.state = kwargs.pop("state", None) + state_id = kwargs.pop("state_id", None) + self.state = CompensationState.objects.get(id=state_id) + super().__init__(*args, **kwargs) self.form_title = _("Edit state") biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None @@ -172,8 +125,8 @@ class RemoveCompensationStateModalForm(RemoveModalForm): state = None def __init__(self, *args, **kwargs): - state = kwargs.pop("state", None) - self.state = state + state_id = kwargs.pop("state_id", None) + self.state = CompensationState.objects.get(id=state_id) super().__init__(*args, **kwargs) def save(self): diff --git a/compensation/tests/compensation/unit/test_forms.py b/compensation/tests/compensation/unit/test_forms.py index dd804b77..0c0831b4 100644 --- a/compensation/tests/compensation/unit/test_forms.py +++ b/compensation/tests/compensation/unit/test_forms.py @@ -190,12 +190,20 @@ class NewCompensationStateModalFormTestCase(BaseTestCase): self.assertEqual(self.compensation.before_states.count(), 0) self.assertEqual(self.compensation.after_states.count(), 0) - form = NewCompensationStateModalForm(data, request=self.request, instance=self.compensation) - + self.request.GET._mutable = True + self.request.GET.update( + { + "before": True, + } + ) + self.request.GET._mutable = False + form = NewCompensationStateModalForm( + data, + request=self.request, + instance=self.compensation, + ) self.assertTrue(form.is_valid(), msg=form.errors) - - is_before_state = True - state = form.save(is_before_state) + state = form.save() self.assertEqual(self.compensation.before_states.count(), 1) self.assertEqual(self.compensation.after_states.count(), 0) @@ -209,8 +217,16 @@ class NewCompensationStateModalFormTestCase(BaseTestCase): self.assertEqual(last_log.action, UserAction.EDITED) self.assertEqual(last_log.comment, ADDED_COMPENSATION_STATE) - is_before_state = False - state = form.save(is_before_state) + self.request.GET._mutable = True + del self.request.GET["before"] + self.request.GET._mutable = False + form = NewCompensationStateModalForm( + data, + request=self.request, + instance=self.compensation, + ) + self.assertTrue(form.is_valid(), msg=form.errors) + state = form.save() self.assertEqual(self.compensation.before_states.count(), 1) self.assertEqual(self.compensation.after_states.count(), 1) @@ -234,7 +250,11 @@ class EditCompensationStateModalFormTestCase(NewCompensationStateModalFormTestCa self.compensation.after_states.add(self.comp_state) def test_init(self): - form = EditCompensationStateModalForm(request=self.request, instance=self.compensation, state=self.comp_state) + form = EditCompensationStateModalForm( + request=self.request, + instance=self.compensation, + state_id=self.comp_state.id + ) self.assertEqual(form.state, self.comp_state) self.assertEqual(form.form_title, str(_("Edit state"))) @@ -265,7 +285,7 @@ class EditCompensationStateModalFormTestCase(NewCompensationStateModalFormTestCa data, request=self.request, instance=self.compensation, - state=self.comp_state + state_id=self.comp_state.id ) self.assertTrue(form.is_valid(), msg=form.errors) @@ -286,7 +306,11 @@ class RemoveCompensationStateModalFormTestCase(EditCompensationStateModalFormTes super().setUp() def test_init(self): - form = RemoveCompensationStateModalForm(request=self.request, instance=self.compensation, state=self.comp_state) + form = RemoveCompensationStateModalForm( + request=self.request, + instance=self.compensation, + state_id=self.comp_state.id + ) self.assertEqual(form.state, self.comp_state) @@ -298,7 +322,7 @@ class RemoveCompensationStateModalFormTestCase(EditCompensationStateModalFormTes data, request=self.request, instance=self.compensation, - state=self.comp_state + state_id=self.comp_state.id ) self.assertTrue(form.is_valid(), msg=form.errors) diff --git a/compensation/views/compensation/state.py b/compensation/views/compensation/state.py index 8fffbbd7..590ddc52 100644 --- a/compensation/views/compensation/state.py +++ b/compensation/views/compensation/state.py @@ -5,46 +5,21 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - from compensation.models import Compensation -from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ AbstractRemoveCompensationStateView class NewCompensationStateView(AbstractNewCompensationStateView): - model = Compensation - redirect_url = "compensation:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _REDIRECT_URL = "compensation:detail" class EditCompensationStateView(AbstractEditCompensationStateView): - model = Compensation - redirect_url = "compensation:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _REDIRECT_URL = "compensation:detail" class RemoveCompensationStateView(AbstractRemoveCompensationStateView): - model = Compensation - redirect_url = "compensation:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(Compensation, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = Compensation + _REDIRECT_URL = "compensation:detail" diff --git a/compensation/views/eco_account/state.py b/compensation/views/eco_account/state.py index 1a28491a..6765810e 100644 --- a/compensation/views/eco_account/state.py +++ b/compensation/views/eco_account/state.py @@ -5,46 +5,21 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - from compensation.models import EcoAccount -from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ AbstractRemoveCompensationStateView class NewEcoAccountStateView(AbstractNewCompensationStateView): - model = EcoAccount - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _REDIRECT_URL = "compensation:acc:detail" class EditEcoAccountStateView(AbstractEditCompensationStateView): - model = EcoAccount - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _REDIRECT_URL = "compensation:acc:detail" class RemoveEcoAccountStateView(AbstractRemoveCompensationStateView): - model = EcoAccount - redirect_url = "compensation:acc:detail" - - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(default_group_required) - @method_decorator(shared_access_required(EcoAccount, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + _MODEL_CLS = EcoAccount + _REDIRECT_URL = "compensation:acc:detail" diff --git a/ema/views/state.py b/ema/views/state.py index e8e489dc..4c3009ed 100644 --- a/ema/views/state.py +++ b/ema/views/state.py @@ -5,46 +5,30 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.utils.decorators import method_decorator - from ema.models import Ema -from konova.decorators import conservation_office_group_required, shared_access_required, login_required_modal from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ AbstractRemoveCompensationStateView class NewEmaStateView(AbstractNewCompensationStateView): - model = Ema - redirect_url = "ema:detail" + _MODEL_CLS = Ema + _REDIRECT_URL = "ema:detail" - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(conservation_office_group_required) - @method_decorator(shared_access_required(Ema, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + def _user_has_permission(self, user): + return user.is_ets_user() class EditEmaStateView(AbstractEditCompensationStateView): - model = Ema - redirect_url = "ema:detail" + _MODEL_CLS = Ema + _REDIRECT_URL = "ema:detail" - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(conservation_office_group_required) - @method_decorator(shared_access_required(Ema, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + def _user_has_permission(self, user): + return user.is_ets_user() class RemoveEmaStateView(AbstractRemoveCompensationStateView): - model = Ema - redirect_url = "ema:detail" + _MODEL_CLS = Ema + _REDIRECT_URL = "ema:detail" - @method_decorator(login_required_modal) - @method_decorator(login_required) - @method_decorator(conservation_office_group_required) - @method_decorator(shared_access_required(Ema, "id")) - def dispatch(self, request, *args, **kwargs): - return super().dispatch(request, *args, **kwargs) + def _user_has_permission(self, user): + return user.is_ets_user() diff --git a/konova/views/state.py b/konova/views/state.py index 1165e08b..419c573b 100644 --- a/konova/views/state.py +++ b/konova/views/state.py @@ -5,103 +5,53 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 22.08.22 """ -from django.shortcuts import get_object_or_404 +from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse -from django.views import View from compensation.forms.modals.state import NewCompensationStateModalForm, EditCompensationStateModalForm, \ RemoveCompensationStateModalForm -from compensation.models import CompensationState from konova.utils.message_templates import COMPENSATION_STATE_ADDED, COMPENSATION_STATE_EDITED, \ COMPENSATION_STATE_REMOVED +from konova.views.base import BaseModalFormView -class AbstractCompensationStateView(View): - model = None - redirect_url = None +class AbstractCompensationStateView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = None + _FORM_CLS = None + _REDIRECT_URL = None class Meta: abstract = True + def _user_has_permission(self, user): + return user.is_default_user() + + def _get_redirect_url(self, *args, **kwargs): + obj = kwargs.get("obj", None) + assert obj is not None + return reverse(self._REDIRECT_URL, args=(obj.id,)) + "#related_data" class AbstractNewCompensationStateView(AbstractCompensationStateView): + _MODEL_CLS = None + _FORM_CLS = NewCompensationStateModalForm + _MSG_SUCCESS = COMPENSATION_STATE_ADDED + class Meta: abstract = True - def get(self, request, id: str): - """ Renders a form for adding new states - - Args: - request (HttpRequest): The incoming request - id (str): The object's id to which the new state will be related - - Returns: - - """ - obj = get_object_or_404(self.model, id=id) - form = NewCompensationStateModalForm(request.POST or None, instance=obj, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_ADDED, - redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" - ) - - def post(self, request, id: str): - return self.get(request, id) - - class AbstractEditCompensationStateView(AbstractCompensationStateView): + _MODEL_CLS = None + _FORM_CLS = EditCompensationStateModalForm + _MSG_SUCCESS = COMPENSATION_STATE_EDITED + class Meta: abstract = True - def get(self, request, id: str, state_id: str): - """ Renders a form for editing a state - - Args: - request (HttpRequest): The incoming request - id (str): The object id - state_id (str): The state's id - - Returns: - - """ - obj = get_object_or_404(self.model, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = EditCompensationStateModalForm(request.POST or None, instance=obj, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_EDITED, - redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" - ) - - def post(self, request, id: str, state_id: str): - return self.get(request, id, state_id) - class AbstractRemoveCompensationStateView(AbstractCompensationStateView): + _MODEL_CLS = None + _FORM_CLS = RemoveCompensationStateModalForm + _MSG_SUCCESS = COMPENSATION_STATE_REMOVED + class Meta: abstract = True - - def get(self, request, id: str, state_id: str): - """ Renders a form for removing astate - - Args: - request (HttpRequest): The incoming request - id (str): The object id - state_id (str): The state's id - - Returns: - - """ - obj = get_object_or_404(self.model, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = RemoveCompensationStateModalForm(request.POST or None, instance=obj, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_REMOVED, - redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data" - ) - - def post(self, request, id: str, state_id: str): - return self.get(request, id, state_id) - -- 2.47.2 From 4a16727da16909d735809122163bcb10d6c248a5 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 4 Nov 2025 08:58:47 +0100 Subject: [PATCH 33/36] # Refactoring revocation views * refactors views for adding, editing and removing revocations * refactors view for getting the document of a revocation * updates tests --- intervention/forms/modals/revocation.py | 10 +- intervention/tests/unit/test_forms.py | 6 +- intervention/urls.py | 12 +-- intervention/views/revocation.py | 134 ++++++++---------------- 4 files changed, 61 insertions(+), 101 deletions(-) diff --git a/intervention/forms/modals/revocation.py b/intervention/forms/modals/revocation.py index 03a0ebe2..498fa529 100644 --- a/intervention/forms/modals/revocation.py +++ b/intervention/forms/modals/revocation.py @@ -7,9 +7,10 @@ Created on: 18.08.22 """ from django import forms from django.core.exceptions import ObjectDoesNotExist +from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ -from intervention.models import RevocationDocument +from intervention.models import RevocationDocument, Revocation from konova.forms.modals import BaseModalForm, RemoveModalForm from konova.utils import validators from konova.utils.message_templates import REVOCATION_ADDED, REVOCATION_EDITED @@ -75,7 +76,8 @@ class EditRevocationModalForm(NewRevocationModalForm): revocation = None def __init__(self, *args, **kwargs): - self.revocation = kwargs.pop("revocation", None) + revocation_id = kwargs.pop("revocation_id", None) + self.revocation = get_object_or_404(Revocation, id=revocation_id) super().__init__(*args, **kwargs) self.form_title = _("Edit revocation") try: @@ -104,8 +106,8 @@ class RemoveRevocationModalForm(RemoveModalForm): revocation = None def __init__(self, *args, **kwargs): - revocation = kwargs.pop("revocation", None) - self.revocation = revocation + revocation_id = kwargs.pop("revocation_id", None) + self.revocation = get_object_or_404(Revocation, id=revocation_id) super().__init__(*args, **kwargs) def save(self): diff --git a/intervention/tests/unit/test_forms.py b/intervention/tests/unit/test_forms.py index 20a6d749..dbf2bf90 100644 --- a/intervention/tests/unit/test_forms.py +++ b/intervention/tests/unit/test_forms.py @@ -280,7 +280,7 @@ class EditRevocationModalFormTestCase(NewRevocationModalFormTestCase): data, request=self.request, instance=self.intervention, - revocation=self.revoc + revocation_id=self.revoc.id ) self.assertTrue(form.is_valid(), msg=form.errors) obj = form.save() @@ -302,7 +302,7 @@ class RemoveRevocationModalFormTestCase(EditRevocationModalFormTestCase): form = RemoveRevocationModalForm( request=self.request, instance=self.intervention, - revocation=self.revoc, + revocation_id=self.revoc.id, ) self.assertEqual(form.instance, self.intervention) self.assertEqual(form.revocation, self.revoc) @@ -317,7 +317,7 @@ class RemoveRevocationModalFormTestCase(EditRevocationModalFormTestCase): data, request=self.request, instance=self.intervention, - revocation=self.revoc + revocation_id=self.revoc.id ) self.assertTrue(form.is_valid(), msg=form.errors) form.save() diff --git a/intervention/urls.py b/intervention/urls.py index 39024f79..10a6e5f0 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -20,8 +20,8 @@ from intervention.views.log import InterventionLogView from intervention.views.record import InterventionRecordView from intervention.views.report import InterventionReportView 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 NewRevocationView, GetRevocationDocumentView, EditRevocationView, \ + RemoveRevocationView from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView app_name = "intervention" @@ -55,10 +55,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', NewRevocationView.as_view(), name='new-revocation'), + path('/revocation//edit', EditRevocationView.as_view(), name='edit-revocation'), + path('/revocation//remove', RemoveRevocationView.as_view(), name='remove-revocation'), + path('revocation/', GetRevocationDocumentView.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..a32bedf5 100644 --- a/intervention/views/revocation.py +++ b/intervention/views/revocation.py @@ -6,113 +6,71 @@ 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 from django.shortcuts import get_object_or_404, redirect -from django.urls import reverse from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \ RemoveRevocationModalForm -from intervention.models import Intervention, RevocationDocument, Revocation -from konova.decorators import default_group_required, shared_access_required, login_required_modal +from intervention.models import Intervention, RevocationDocument from konova.utils.documents import get_document -from konova.utils.message_templates import REVOCATION_ADDED, DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED +from konova.utils.message_templates import DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED, REVOCATION_ADDED +from konova.views.base import BaseModalFormView, BaseView -@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 BaseRevocationView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = Intervention + _REDIRECT_URL = "intervention:detail" - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id + class Meta: + abstract = True - Returns: + def _user_has_permission(self, user): + return user.is_default_user() - """ - 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" - ) + def _get_redirect_url(self, *args, **kwargs): + url = super()._get_redirect_url(*args, **kwargs) + return f"{url}#related_data" -@login_required -@default_group_required -def get_revocation_view(request: HttpRequest, doc_id: str): - """ Returns the revocation document as downloadable file - - Wraps the generic document fetcher function from konova.utils. - - Args: - request (HttpRequest): The incoming request - doc_id (str): The document id - - Returns: - - """ - doc = get_object_or_404(RevocationDocument, id=doc_id) - # File download only possible if related instance is shared with user - if not doc.instance.legal.intervention.users.filter(id=request.user.id): - messages.info( - request, - DATA_UNSHARED - ) - return redirect("intervention:detail", id=doc.instance.id) - return get_document(doc) +class NewRevocationView(BaseRevocationView): + _FORM_CLS = NewRevocationModalForm + _MSG_SUCCESS = REVOCATION_ADDED -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def edit_revocation_view(request: HttpRequest, id: str, revocation_id: str): - """ Renders a edit view for a revocation - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id as string - revocation_id (str): The revocation's id as string - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - revocation = get_object_or_404(Revocation, id=revocation_id) - - form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, revocation=revocation, request=request) - return form.process_request( - request, - REVOCATION_EDITED, - redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" - ) +class EditRevocationView(BaseRevocationView): + _FORM_CLS = EditRevocationModalForm + _MSG_SUCCESS = REVOCATION_EDITED -@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 RemoveRevocationView(BaseRevocationView): + _FORM_CLS = RemoveRevocationModalForm + _MSG_SUCCESS = REVOCATION_REMOVED - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id as string - revocation_id (str): The revocation's id as string - Returns: +class GetRevocationDocumentView(LoginRequiredMixin, BaseView): + _MODEL_CLS = RevocationDocument + _REDIRECT_URL = "intervention:detail" - """ - intervention = get_object_or_404(Intervention, id=id) - revocation = get_object_or_404(Revocation, id=revocation_id) + def get(self, request: HttpRequest, doc_id: str): + 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) - 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" - ) + def _user_has_permission(self, user): + return user.is_default_user() + def _user_has_shared_access(self, user, **kwargs): + obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get("doc_id")) + assert obj is not None + return obj.instance.intervention.is_shared_with(user) + + def _get_redirect_url(self, *args, **kwargs): + url = super()._get_redirect_url(*args, **kwargs) + return f"{url}#related_data" -- 2.47.2 From bc2e901ca91d5ecb28ef2e5f078a19a5c4337dcb Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 4 Nov 2025 09:09:05 +0100 Subject: [PATCH 34/36] # Refactoring payment view * refactors views for adding, editing and removing payments --- compensation/forms/modals/payment.py | 9 ++- compensation/urls/payment.py | 8 +-- compensation/views/payment.py | 88 +++++++--------------------- 3 files changed, 31 insertions(+), 74 deletions(-) diff --git a/compensation/forms/modals/payment.py b/compensation/forms/modals/payment.py index eed2e702..fa69dd15 100644 --- a/compensation/forms/modals/payment.py +++ b/compensation/forms/modals/payment.py @@ -6,8 +6,10 @@ Created on: 18.08.22 """ from django import forms +from django.shortcuts import get_object_or_404 from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _ +from compensation.models import Payment from konova.forms.modals import RemoveModalForm, BaseModalForm from konova.utils import validators from konova.utils.message_templates import PAYMENT_EDITED @@ -103,7 +105,8 @@ class EditPaymentModalForm(NewPaymentForm): payment = None def __init__(self, *args, **kwargs): - self.payment = kwargs.pop("payment", None) + payment_id = kwargs.pop("payment_id", None) + self.payment = get_object_or_404(Payment, id=payment_id) super().__init__(*args, **kwargs) self.form_title = _("Edit payment") form_date = { @@ -133,8 +136,8 @@ class RemovePaymentModalForm(RemoveModalForm): payment = None def __init__(self, *args, **kwargs): - payment = kwargs.pop("payment", None) - self.payment = payment + payment_id = kwargs.pop("payment_id", None) + self.payment = get_object_or_404(Payment, id=payment_id) super().__init__(*args, **kwargs) def save(self): diff --git a/compensation/urls/payment.py b/compensation/urls/payment.py index b51384dd..2c4d39a6 100644 --- a/compensation/urls/payment.py +++ b/compensation/urls/payment.py @@ -6,11 +6,11 @@ Created on: 24.08.21 """ from django.urls import path -from compensation.views.payment import * +from compensation.views.payment import NewPaymentView, RemovePaymentView, EditPaymentView 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..49ea8ede 100644 --- a/compensation/views/payment.py +++ b/compensation/views/payment.py @@ -5,84 +5,38 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 09.08.21 """ -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.contrib.auth.mixins import LoginRequiredMixin from compensation.forms.modals.payment import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm -from compensation.models import Payment from intervention.models import Intervention -from konova.decorators import default_group_required, shared_access_required from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED +from konova.views.base import BaseModalFormView -@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 BasePaymentView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = Intervention + _REDIRECT_URL = "intervention:detail" - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id for which a new payment shall be added + class Meta: + abstract = True - Returns: + def _get_redirect_url(self, *args, **kwargs): + url = super()._get_redirect_url(*args, **kwargs) + return f"{url}#related_data" - """ - 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" - ) + def _user_has_permission(self, user): + return user.is_default_user() -@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 - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - payment_id (str): The payment's id - - 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" - ) +class NewPaymentView(BasePaymentView): + _FORM_CLS = NewPaymentForm + _MSG_SUCCESS = PAYMENT_ADDED -@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 - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - payment_id (str): The payment's id - - 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" - ) +class EditPaymentView(BasePaymentView): + _MSG_SUCCESS = PAYMENT_EDITED + _FORM_CLS = EditPaymentModalForm +class RemovePaymentView(BasePaymentView): + _MSG_SUCCESS = PAYMENT_REMOVED + _FORM_CLS = RemovePaymentModalForm -- 2.47.2 From f122778232c345f47421bf437f622cee786518ac Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 5 Nov 2025 10:12:49 +0100 Subject: [PATCH 35/36] # Refactoring team views * refactors team views * split views.py into users.py and teams.py in users app * refactors method headers for _user_has_permission() * adds method and class comments and documentation to base view classes --- .../views/compensation/compensation.py | 6 +- compensation/views/eco_account/eco_account.py | 6 +- compensation/views/payment.py | 2 +- ema/views/action.py | 6 +- ema/views/deadline.py | 6 +- ema/views/document.py | 8 +- ema/views/ema.py | 8 +- ema/views/log.py | 2 +- ema/views/resubmission.py | 2 +- ema/views/share.py | 2 +- ema/views/state.py | 6 +- intervention/views/check.py | 2 +- intervention/views/revocation.py | 4 +- konova/utils/message_templates.py | 6 + konova/views/action.py | 2 +- konova/views/base.py | 321 +++++++++++++++--- konova/views/deadline.py | 6 +- konova/views/deduction.py | 2 +- konova/views/detail.py | 2 +- konova/views/document.py | 8 +- konova/views/geometry.py | 4 +- konova/views/home.py | 2 +- konova/views/log.py | 2 +- konova/views/record.py | 2 +- konova/views/remove.py | 2 +- konova/views/report.py | 2 +- konova/views/resubmission.py | 2 +- konova/views/share.py | 4 +- konova/views/state.py | 2 +- user/urls.py | 12 +- user/views/teams.py | 105 ++++++ user/views/users.py | 81 +++++ user/views/views.py | 206 ----------- 33 files changed, 519 insertions(+), 314 deletions(-) create mode 100644 user/views/teams.py create mode 100644 user/views/users.py delete mode 100644 user/views/views.py diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 098849d8..257eaf8b 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -59,7 +59,7 @@ class NewCompensationFormView(BaseNewSpatialLocatedObjectFormView): intervention = get_object_or_404(Intervention, id=intervention_id) return intervention.is_shared_with(user) - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): # User has to be an ets user return user.is_default_user() @@ -88,7 +88,7 @@ class EditCompensationFormView(BaseEditSpatialLocatedObjectFormView): _TEMPLATE = "compensation/form/view.html" _REDIRECT_URL = "compensation:detail" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): # User has to be a default user return user.is_default_user() @@ -170,5 +170,5 @@ class RemoveCompensationView(LoginRequiredMixin, BaseRemoveModalFormView): _FORM_CLS = RemoveModalForm _REDIRECT_URL = "compensation:index" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index f9c50ab8..9a40b0b1 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -49,7 +49,7 @@ class NewEcoAccountFormView(BaseNewSpatialLocatedObjectFormView): _TAB_TITLE = _("New Eco-Account") _REDIRECT_URL = "compensation:acc:detail" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): # User has to be a default user return user.is_default_user() @@ -60,7 +60,7 @@ class EditEcoAccountFormView(BaseEditSpatialLocatedObjectFormView): _TEMPLATE = "compensation/form/view.html" _REDIRECT_URL = "compensation:acc:detail" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): # User has to be a default user return user.is_default_user() @@ -260,5 +260,5 @@ class RemoveEcoAccountView(LoginRequiredMixin, BaseRemoveModalFormView): _FORM_CLS = RemoveEcoAccountModalForm _REDIRECT_URL = "compensation:acc:index" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() diff --git a/compensation/views/payment.py b/compensation/views/payment.py index 49ea8ede..57957870 100644 --- a/compensation/views/payment.py +++ b/compensation/views/payment.py @@ -24,7 +24,7 @@ class BasePaymentView(LoginRequiredMixin, BaseModalFormView): url = super()._get_redirect_url(*args, **kwargs) return f"{url}#related_data" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() diff --git a/ema/views/action.py b/ema/views/action.py index e09511fd..8fc6c595 100644 --- a/ema/views/action.py +++ b/ema/views/action.py @@ -16,14 +16,14 @@ class NewEmaActionView(AbstractNewCompensationActionView): _MODEL_CLS = Ema _REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() class EditEmaActionView(AbstractEditCompensationActionView): _MODEL_CLS = Ema _REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() @@ -31,5 +31,5 @@ class RemoveEmaActionView(AbstractRemoveCompensationActionView): _MODEL_CLS = Ema _REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() diff --git a/ema/views/deadline.py b/ema/views/deadline.py index 475d1ffa..21ad0bf2 100644 --- a/ema/views/deadline.py +++ b/ema/views/deadline.py @@ -14,7 +14,7 @@ class NewEmaDeadlineView(AbstractNewDeadlineView): _MODEL_CLS = Ema _REDIRECT_URL = _EMA_DETAIL_URL_NAME - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() @@ -22,7 +22,7 @@ class EditEmaDeadlineView(AbstractEditDeadlineView): _MODEL_CLS = Ema _REDIRECT_URL = _EMA_DETAIL_URL_NAME - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() @@ -30,5 +30,5 @@ class RemoveEmaDeadlineView(AbstractRemoveDeadlineView): _MODEL_CLS = Ema _REDIRECT_URL = _EMA_DETAIL_URL_NAME - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() diff --git a/ema/views/document.py b/ema/views/document.py index 6f94723a..002304f0 100644 --- a/ema/views/document.py +++ b/ema/views/document.py @@ -16,14 +16,14 @@ class NewEmaDocumentView(AbstractNewDocumentView): _FORM_CLS = NewEmaDocumentModalForm _REDIRECT_URL = "ema:detail" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() class GetEmaDocumentView(AbstractGetDocumentView): _MODEL_CLS = Ema _DOCUMENT_CLS = EmaDocument - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() class RemoveEmaDocumentView(AbstractRemoveDocumentView): @@ -32,7 +32,7 @@ class RemoveEmaDocumentView(AbstractRemoveDocumentView): _FORM_CLS = RemoveEmaDocumentModalForm _REDIRECT_URL = "ema:detail" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() class EditEmaDocumentView(AbstractEditDocumentView): @@ -41,5 +41,5 @@ class EditEmaDocumentView(AbstractEditDocumentView): _DOCUMENT_CLS = EmaDocument _REDIRECT_URL = "ema:detail" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() diff --git a/ema/views/ema.py b/ema/views/ema.py index 6057a95c..c211b799 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -38,7 +38,7 @@ class NewEmaFormView(BaseNewSpatialLocatedObjectFormView): _TAB_TITLE = _("New EMA") _REDIRECT_URL = "ema:detail" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): # User has to be an ets user return user.is_ets_user() @@ -50,7 +50,7 @@ class EditEmaFormView(BaseEditSpatialLocatedObjectFormView): _REDIRECT_URL = "ema:detail" _TAB_TITLE = _("Edit {}") - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): # User has to be an ets user return user.is_ets_user() @@ -59,7 +59,7 @@ class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView _MODEL_CLS = Ema _REDIRECT_URL = "ema:index" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() @@ -112,5 +112,5 @@ class RemoveEmaView(LoginRequiredMixin, BaseRemoveModalFormView): _MODEL_CLS = Ema _REDIRECT_URL = "ema:index" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() diff --git a/ema/views/log.py b/ema/views/log.py index 3f0ca939..3baf3886 100644 --- a/ema/views/log.py +++ b/ema/views/log.py @@ -14,5 +14,5 @@ from konova.views.log import AbstractLogView class EmaLogView(LoginRequiredMixin, AbstractLogView): _MODEL_CLS = Ema - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() diff --git a/ema/views/resubmission.py b/ema/views/resubmission.py index 76ea60bd..fb499859 100644 --- a/ema/views/resubmission.py +++ b/ema/views/resubmission.py @@ -16,5 +16,5 @@ class EmaResubmissionView(AbstractResubmissionView): _REDIRECT_URL = "ema:detail" action_url = "ema:resubmission-create" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() diff --git a/ema/views/share.py b/ema/views/share.py index 84ebfdbf..39fc109d 100644 --- a/ema/views/share.py +++ b/ema/views/share.py @@ -17,5 +17,5 @@ class EmaShareFormView(AbstractShareFormView): _MODEL_CLS = Ema _REDIRECT_URL = "ema:detail" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() \ No newline at end of file diff --git a/ema/views/state.py b/ema/views/state.py index 4c3009ed..f28ca698 100644 --- a/ema/views/state.py +++ b/ema/views/state.py @@ -14,7 +14,7 @@ class NewEmaStateView(AbstractNewCompensationStateView): _MODEL_CLS = Ema _REDIRECT_URL = "ema:detail" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() @@ -22,7 +22,7 @@ class EditEmaStateView(AbstractEditCompensationStateView): _MODEL_CLS = Ema _REDIRECT_URL = "ema:detail" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() @@ -30,5 +30,5 @@ class RemoveEmaStateView(AbstractRemoveCompensationStateView): _MODEL_CLS = Ema _REDIRECT_URL = "ema:detail" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() diff --git a/intervention/views/check.py b/intervention/views/check.py index 07387913..09efc105 100644 --- a/intervention/views/check.py +++ b/intervention/views/check.py @@ -19,7 +19,7 @@ class InterventionCheckView(LoginRequiredMixin, BaseModalFormView): _MSG_SUCCESS = _("Check performed") _REDIRECT_URL = "intervention:detail" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_zb_user() def _get_redirect_url(self, *args, **kwargs): diff --git a/intervention/views/revocation.py b/intervention/views/revocation.py index a32bedf5..26f30c05 100644 --- a/intervention/views/revocation.py +++ b/intervention/views/revocation.py @@ -25,7 +25,7 @@ class BaseRevocationView(LoginRequiredMixin, BaseModalFormView): class Meta: abstract = True - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() def _get_redirect_url(self, *args, **kwargs): @@ -63,7 +63,7 @@ class GetRevocationDocumentView(LoginRequiredMixin, BaseView): return redirect("intervention:detail", id=doc.instance.id) return get_document(doc) - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() def _user_has_shared_access(self, user, **kwargs): diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py index a7e05c53..8e16fb41 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -20,6 +20,12 @@ ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office us MISSING_GROUP_PERMISSION = _("You need to be part of another user group.") CHECK_STATE_RESET = _("Status of Checked reset") +# USER | TEAM +TEAM_ADDED = _("New team added") +TEAM_EDITED = _("Team edited") +TEAM_REMOVED = _("Team removed") +TEAM_LEFT = _("Left Team") + # REMOVED GENERIC_REMOVED_TEMPLATE = _("{} removed") diff --git a/konova/views/action.py b/konova/views/action.py index cd00187d..63b44995 100644 --- a/konova/views/action.py +++ b/konova/views/action.py @@ -21,7 +21,7 @@ class AbstractCompensationActionView(LoginRequiredMixin, BaseModalFormView): class Meta: abstract = True - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() def _get_redirect_url(self, *args, **kwargs): diff --git a/konova/views/base.py b/konova/views/base.py index 5f9db037..0c7532ee 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -8,6 +8,7 @@ from abc import abstractmethod from bootstrap_modal_forms.mixins import is_ajax from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin +from django.core.exceptions import ObjectDoesNotExist from django.http import HttpRequest, JsonResponse, HttpResponseRedirect from django.shortcuts import render, redirect, get_object_or_404 from django.urls import reverse @@ -24,18 +25,37 @@ from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHAR class BaseView(View): - _TEMPLATE: str = "CHANGE_ME" - _TAB_TITLE: str = "CHANGE_ME" - _REDIRECT_URL: str = "CHANGE_ME" - _REDIRECT_URL_ERROR: str = "home" + """ An abstract base view + + This class represents the root of all views on this project. It defines private variables which have to be used + by inheriting classes for proper generic inheriting. + + """ + _TEMPLATE: str = "CHANGE_ME" # Path to template file + _TAB_TITLE: str = "CHANGE_ME" # Title displayed on browser tab + _REDIRECT_URL: str = "CHANGE_ME" # Default URL to redirect after processing (notation as django url "namespace:endpoint") + _REDIRECT_URL_ERROR: str = "home" # Default URL to redirect in case of an error (same notation) class Meta: abstract = True def dispatch(self, request, *args, **kwargs): + """ Dispatching requests before forwarding them into GET or POST endpoints. + + Defines basic checks which need to be done before a user can get access to any view inheriting from + this class. + + Args: + request (HttpRequest): The incoming request + *args (): + **kwargs (): + + Returns: + + """ request = check_user_is_in_any_group(request) - if not self._user_has_permission(request.user): + if not self._user_has_permission(request.user, **kwargs): messages.info(request, MISSING_GROUP_PERMISSION) return redirect(reverse(self._REDIRECT_URL_ERROR)) @@ -46,37 +66,68 @@ class BaseView(View): return super().dispatch(request, *args, **kwargs) @abstractmethod - def _user_has_permission(self, user): - """ Has to be implemented properly by inheriting classes + def _user_has_permission(self, user, **kwargs): + """ Checks whether the user has permission to get this view rendered. + + If no specific check is needed, this method can be overwritten with a simple True returning. Args: - user (): + user (User): The performing user + **kwargs (): Returns: - + has_permission (bool): Whether the user has permission to see this view """ raise NotImplementedError("User permission not checked!") @abstractmethod def _user_has_shared_access(self, user, **kwargs): - """ Has to be implemented properly by inheriting classes + """ Checks whether the user has shared access to this object. + + If no shared-access-check is needed, this method can be overwritten with a simple True returning. Args: - user (): + user (User): The performing user + **kwargs (): Returns: - + has_shared_access (bool): Whether the user has shared access """ raise NotImplementedError("Shared access not checked!") def _get_redirect_url(self, *args, **kwargs): + """ Getter to construct a more specific, data dependant redirect URL + + By default the method simply returns the pre-defined redirect URL. + + Args: + *args (): + **kwargs (): + + Returns: + url (str): Reversed redirect url + """ return self._REDIRECT_URL def _get_redirect_url_error(self, *args, **kwargs): + """ Getter to construct a more specific, data dependant redirect URL in error cases + + By default the method simply returns the pre-defined redirect URL for errors. + + Args: + *args (): + **kwargs (): + + Returns: + url (str): Reversed redirect url + """ return self._REDIRECT_URL_ERROR class BaseModalFormView(BaseView): - _TEMPLATE = "modal/modal_form.html" + """ Abstract base view providing logic to perform most modal form based view renderings + + """ + _TEMPLATE: str = "modal/modal_form.html" _MODEL_CLS = None _FORM_CLS = None _MSG_SUCCESS = None @@ -85,12 +136,45 @@ class BaseModalFormView(BaseView): abstract = True def _user_has_shared_access(self, user, **kwargs): + """ Checks whether the user has shared access to this object. + + For objects inheriting from BaseObject class the method 'is_shared_with()' is a handy + wrapper for checking shared access. For any other circumstances this method should be overwritten + to provide custom shared-access-checking logic. + + If no shared-access-check is needed, this method can be overwritten with a simple True returning. + + Args: + user (User): The performing user + **kwargs (): + + Returns: + has_shared_access (bool): Whether the user has shared access + """ obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get("id")) return obj.is_shared_with(user) - def get(self, request: HttpRequest, id: str, *args, **kwargs): - obj = self._MODEL_CLS.objects.get(id=id) - self._check_for_recorded_instance(obj) + def get(self, request: HttpRequest, *args, **kwargs): + """ GET endpoint for rendering a view holding a modal form + + Args: + request (HttpRequest): The incoming request + *args (): + **kwargs (): + + Returns: + + """ + # If there is an id provided as mapped parameter from the URL take it ... + _id = kwargs.pop("id", None) + try: + # ... and try to resolve it into a record + obj = self._MODEL_CLS.objects.get(id=_id) + self._check_for_recorded_instance(obj) + except ObjectDoesNotExist: + # ... If there is none, maybe we are currently processing + # the creation of a new object (therefore no id yet), so let's continue + obj = None form = self._FORM_CLS( request.POST or None, request.FILES or None, @@ -104,9 +188,27 @@ class BaseModalFormView(BaseView): context = BaseContext(request, context).context return render(request, self._TEMPLATE, context) - def post(self, request: HttpRequest, id: str, *args, **kwargs): - obj = self._MODEL_CLS.objects.get(id=id) - self._check_for_recorded_instance(obj) + def post(self, request: HttpRequest, *args, **kwargs): + """ POST endpoint for processing form contents of a view + + Args: + request (HttpRequest): The incoming request + *args (): + **kwargs (): + + Returns: + + """ + # If there is an id provided as mapped parameter from the URL take it ... + _id = kwargs.pop("id", None) + try: + # ... and try to resolve it into a record + obj = self._MODEL_CLS.objects.get(id=_id) + self._check_for_recorded_instance(obj) + except ObjectDoesNotExist: + # ... If there is none, maybe we are currently processing + # the creation of a new object (therefore no id yet), so let's continue + obj = None form = self._FORM_CLS( request.POST or None, request.FILES or None, @@ -114,13 +216,15 @@ class BaseModalFormView(BaseView): request=request, **kwargs ) + # Get now the redirect url and take specifics of the obj into account for that. + # We do not do this after saving the form to avoid side effects due to possibly changed data redirect_url = self._get_redirect_url(obj=obj) if form.is_valid(): + # Modal forms send one POST for checking on data validity. This is used to evaluate possible errors + # on the form. The second POST (if no errors have been found) is the 'proper' one, + # which we want to process by saving/commiting of the data to the database. if not is_ajax(request.META): - # Modal forms send one POST for checking on data validity. This can be used to return possible errors - # on the form. A second POST (if no errors occurs) is sent afterward and needs to process the - # saving/commiting of the data to the database. is_ajax() performs this check. The first request is - # an ajax call, the second is a regular form POST. + # Get now the success message and take specifics of the obj into account for that msg_success = self._get_msg_success(obj=obj, *args, **kwargs) form.save() messages.success( @@ -136,20 +240,41 @@ class BaseModalFormView(BaseView): return render(request, self._TEMPLATE, context) def _get_redirect_url(self, *args, **kwargs): + """ Getter to construct a more specific, data dependant redirect URL (if needed) + + Args: + *args (): + **kwargs (): + + Returns: + url (str): Reversed redirect url + """ obj = kwargs.get("obj", None) - assert obj is not None - return reverse(self._REDIRECT_URL, args=(obj.id,)) + if obj: + return reverse(self._REDIRECT_URL, args=(obj.id,)) + else: + return reverse(self._REDIRECT_URL) def _get_msg_success(self, *args, **kwargs): + """ Getter to construct a more specific, data dependant success message + + Args: + *args (): + **kwargs (): + + Returns: + + """ return self._MSG_SUCCESS def _check_for_recorded_instance(self, obj): - """ Checks if the object on this view is recorded and runs some special logic if yes + """ Checks if the object on this view is recorded and runs some special logic if so If the instance is recorded, the view should provide some information about why the user can not edit anything. + This behaviour is only intended to mask any form for instances based on the BaseObject class. - There are situations where the form should be rendered regularly, - e.g deduction forms for (recorded) eco accounts. + There are situations where the form should be rendered regularly, despite the instance being recorded, + e.g. for rendering deduction form contents on (recorded) eco accounts. Returns: @@ -162,29 +287,32 @@ class BaseModalFormView(BaseView): return if obj.is_recorded: - self._block_form() + # Replace default template with a blocking one + self._TEMPLATE = "form/recorded_no_edit.html" - def _block_form(self): - """ - Overwrites template, providing no actions - - Returns: - - """ - self._TEMPLATE = "form/recorded_no_edit.html" class BaseIndexView(BaseView): - """ Base class for index views + """ Abstract base class for index views """ - _TEMPLATE = "generic_index.html" + _TEMPLATE: str = "generic_index.html" _INDEX_TABLE_CLS = None - _REDIRECT_URL = "home" + _REDIRECT_URL: str = "home" class Meta: abstract = True - def get(self, request: HttpRequest): + def get(self, request: HttpRequest, *args, **kwargs): + """ GET endpoint for rendering index views + + Args: + request (HttpRequest): The incoming request + *args (): + **kwargs (): + + Returns: + + """ qs = self._get_queryset() table = self._INDEX_TABLE_CLS( request=request, @@ -199,9 +327,14 @@ class BaseIndexView(BaseView): @abstractmethod def _get_queryset(self): + """ Generic getter for the queryset of objects which shall be processed on this view + + Returns: + + """ raise NotImplementedError - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): # No specific permissions needed for opening base index view return True @@ -228,7 +361,7 @@ class BaseIdentifierGeneratorView(BaseView): } ) - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): """ Should be overwritten in inheriting classes! Args: @@ -245,6 +378,9 @@ class BaseIdentifierGeneratorView(BaseView): class BaseFormView(BaseView): + """ Abstract base class for rendering form views + + """ _MODEL_CLS = None _FORM_CLS = None @@ -252,18 +388,21 @@ class BaseFormView(BaseView): abstract = True def _get_additional_context(self, **kwargs): - """ + """ Getter for additional data, which is needed to properly render the current view Args: **kwargs (): Returns: - + context (dict): Additional context data for rendering """ return {} class BaseSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView): + """ Abstract base view for processing objects with spatial data + + """ _GEOMETRY_FORM_CLS = SimpleGeomForm class Meta: @@ -271,8 +410,11 @@ class BaseSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView): class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): + """ Base view for creating new spatial data related to objects - def _user_has_permission(self, user): + """ + + def _user_has_permission(self, user, **kwargs): # User has to have default privilege to call this endpoint return user.is_default_user() @@ -280,10 +422,21 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): # There is no shared access control since nothing exists yet return True - def get(self, request: HttpRequest, **kwargs): + def get(self, request: HttpRequest, *args, **kwargs): + """ GET endpoint for rendering a form view where object data and spatial data are processed + + Args: + request (HttpRequest): The incoming request + **kwargs (): + + Returns: + + """ + # First initialize the regular object form and the geometry form based on request-bound data form: BaseForm = self._FORM_CLS(None, **kwargs, user=request.user) geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, user=request.user, read_only=False) + # Get some additional context and put everything into the rendering pipeline context = self._get_additional_context() context = BaseContext(request, additional_context=context).context context.update( @@ -295,16 +448,30 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): ) return render(request, self._TEMPLATE, context) - def post(self, request: HttpRequest, **kwargs): + def post(self, request: HttpRequest, *args, **kwargs): + """ POST endpoint for processing object and spatial data provided by forms + + Args: + request (HttpRequest): The incoming request + **kwargs (): + + Returns: + + """ + # First initialize the regular object form and the geometry form based on request-bound data form: BaseForm = self._FORM_CLS(request.POST or None, **kwargs, user=request.user) geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(request.POST or None, user=request.user, read_only=False) + # Only continue if both forms are without errors if form.is_valid() and geom_form.is_valid(): obj = form.save(request.user, geom_form) obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,)) generated_identifier = form.cleaned_data.get("identifier", None) + # There is a rare chance that an identifier has been taken already between sending the form and processing + # the data. If the identifier can not be used anymore, we have to inform the user that another identifier + # had to be generated if generated_identifier != obj.identifier: messages.info( request, @@ -314,12 +481,18 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): ) ) messages.success(request, _("{} added").format(obj.identifier)) + # Very complex geometries have to be simplified automatically while processing the spatial data. If this + # is the case, the user has to be informed. (They might want to check whether the stored geometry still + # fits their needs) if geom_form.has_geometry_simplified(): messages.info( request, GEOMETRY_SIMPLIFIED ) + # If certain parts of the geometry do not pass the quality check (e.g. way too small and therefore more like + # cutting errors) we need to inform the user that some parts have been removed/ignored while storing the + # geometry num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( @@ -329,6 +502,7 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): return redirect(obj_redirect_url) else: + # Something was not properly entered on the forms, so we have to inform the user context = self._get_additional_context() messages.error(request, FORM_INVALID, extra_tags="danger",) @@ -344,15 +518,30 @@ class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): + """ Base view for editing new spatial data related to objects + + """ _TAB_TITLE = _("Edit {}") - def get(self, request: HttpRequest, id: str): + def get(self, request: HttpRequest, id: str, *args, **kwargs): + """ GET endpoint for rendering a form view where object data and spatial data are processed + + Args: + request (HttpRequest): The incoming request + id (str): The id of the object (not the geometry) + + Returns: + + """ + # First fetch the object identified by the id obj = get_object_or_404( self._MODEL_CLS, id=id ) obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,)) + # Check whether the object is recorded. If so - we can redirect the user and inform about the un-editability + # of this entry if obj.is_recorded: messages.info( request, @@ -360,9 +549,11 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): ) return redirect(obj_redirect_url) + # Seems like the object is not recorded. Good - initialize the forms based on the obj and request-bound data form: BaseForm = self._FORM_CLS(None, instance=obj, user=request.user) geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, instance=obj, read_only=False) + # Get additional context for rendering and put everything in the rendering pipeline context = self._get_additional_context() context = BaseContext(request, additional_context=context).context context.update( @@ -374,13 +565,33 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): ) return render(request, self._TEMPLATE, context) - def post(self, request: HttpRequest, id: str): + def post(self, request: HttpRequest, id: str, *args, **kwargs): + """ POST endpoint for processing object and spatial data provided by forms + + Args: + request (HttpRequest): The incoming request + id (str): The object's id + *args (): + **kwargs (): + + Returns: + + """ obj = get_object_or_404( self._MODEL_CLS, id=id ) obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,)) + # If the object is recorded, we abort the processing directly and inform the user + if obj.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect(obj_redirect_url) + + # Initialize forms with obj and request-bound data form: BaseForm = self._FORM_CLS(request.POST or None, instance=obj, user=request.user) geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(request.POST or None, instance=obj, read_only=False) @@ -388,12 +599,18 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): obj = form.save(request.user, geom_form) messages.success(request, _("{} edited").format(obj.identifier)) + # Very complex geometries have to be simplified automatically while processing the spatial data. If this + # is the case, the user has to be informed. (They might want to check whether the stored geometry still + # fits their needs) if geom_form.has_geometry_simplified(): messages.info( request, GEOMETRY_SIMPLIFIED ) + # If certain parts of the geometry do not pass the quality check (e.g. way too small and therefore more like + # cutting errors) we need to inform the user that some parts have been removed/ignored while storing the + # geometry num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( @@ -420,5 +637,5 @@ class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get('id', None)) return obj.is_shared_with(user) - def _user_has_permission(self, user): - return user.is_default_user() \ No newline at end of file + def _user_has_permission(self, user, **kwargs): + return user.is_default_user() diff --git a/konova/views/deadline.py b/konova/views/deadline.py index 382c5f77..5258e72f 100644 --- a/konova/views/deadline.py +++ b/konova/views/deadline.py @@ -25,7 +25,7 @@ class AbstractNewDeadlineView(LoginRequiredMixin, BaseModalFormView): def _get_redirect_url(self, *args, **kwargs): return super()._get_redirect_url(*args, **kwargs) + "#related_data" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() @@ -41,7 +41,7 @@ class AbstractEditDeadlineView(LoginRequiredMixin, BaseModalFormView): def _get_redirect_url(self, *args, **kwargs): return super()._get_redirect_url(*args, **kwargs) + "#related_data" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() @@ -57,5 +57,5 @@ class AbstractRemoveDeadlineView(LoginRequiredMixin, BaseModalFormView): def _get_redirect_url(self, *args, **kwargs): return super()._get_redirect_url(*args, **kwargs) + "#related_data" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() diff --git a/konova/views/deduction.py b/konova/views/deduction.py index b7b9968c..73bc9817 100644 --- a/konova/views/deduction.py +++ b/konova/views/deduction.py @@ -28,7 +28,7 @@ class AbstractDeductionView(BaseModalFormView): """ pass - def _user_has_permission(self, user) -> bool: + def _user_has_permission(self, user, **kwargs) -> bool: """ Args: diff --git a/konova/views/detail.py b/konova/views/detail.py index 72b4ad17..e324231f 100644 --- a/konova/views/detail.py +++ b/konova/views/detail.py @@ -42,7 +42,7 @@ class BaseDetailView(LoginRequiredMixin, BaseView): # Access to an entry's detail view is not restricted by the state of being-shared or not return True - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): # Detail views have no restrictions return True diff --git a/konova/views/document.py b/konova/views/document.py index 09df39c7..f80e399d 100644 --- a/konova/views/document.py +++ b/konova/views/document.py @@ -27,7 +27,7 @@ class AbstractNewDocumentView(LoginRequiredMixin, BaseModalFormView): def _get_redirect_url(self, *args, **kwargs): return super()._get_redirect_url(*args, **kwargs) + "#related_data" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() @@ -58,7 +58,7 @@ class AbstractGetDocumentView(LoginRequiredMixin, BaseView): def post(self, request, id: str, doc_id: str): return self.get(request, id, doc_id) - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() def _user_has_shared_access(self, user, **kwargs): @@ -80,7 +80,7 @@ class AbstractRemoveDocumentView(LoginRequiredMixin, BaseModalFormView): def _get_redirect_url(self, *args, **kwargs): return super()._get_redirect_url(*args, **kwargs) + "#related_data" - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() def _get_msg_success(self, *args, **kwargs): @@ -100,7 +100,7 @@ class AbstractEditDocumentView(LoginRequiredMixin, BaseModalFormView): class Meta: abstract = True - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() def _get_redirect_url(self, *args, **kwargs): diff --git a/konova/views/geometry.py b/konova/views/geometry.py index 087c5af3..1c25131f 100644 --- a/konova/views/geometry.py +++ b/konova/views/geometry.py @@ -110,7 +110,7 @@ class GeomParcelsView(BaseView): def _user_has_shared_access(self, user, **kwargs): return True - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return True @@ -160,5 +160,5 @@ class GeomParcelsContentView(BaseView): def _user_has_shared_access(self, user, **kwargs): return True - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return True diff --git a/konova/views/home.py b/konova/views/home.py index 35cbcb1b..0c0772ea 100644 --- a/konova/views/home.py +++ b/konova/views/home.py @@ -74,7 +74,7 @@ class HomeView(LoginRequiredMixin, BaseView): context = BaseContext(request, additional_context).context return render(request, self._TEMPLATE, context) - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): # No specific permission needed for home view return True diff --git a/konova/views/log.py b/konova/views/log.py index df898808..ab1a0498 100644 --- a/konova/views/log.py +++ b/konova/views/log.py @@ -46,5 +46,5 @@ class AbstractLogView(BaseView): obj = get_object_or_404(self._MODEL_CLS, id=obj_id) return obj.is_shared_with(user) - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() diff --git a/konova/views/record.py b/konova/views/record.py index cf780acb..f4dd9e19 100644 --- a/konova/views/record.py +++ b/konova/views/record.py @@ -15,7 +15,7 @@ class AbstractRecordView(BaseModalFormView): _FORM_CLS = RecordModalForm _MSG_SUCCESS = None - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_ets_user() def _get_msg_success(self, *args, **kwargs): diff --git a/konova/views/remove.py b/konova/views/remove.py index 82b1e57a..32b3928a 100644 --- a/konova/views/remove.py +++ b/konova/views/remove.py @@ -16,7 +16,7 @@ class BaseRemoveModalFormView(BaseModalFormView): _MSG_SUCCESS = GENERIC_REMOVED_TEMPLATE _REDIRECT_URL = None - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() def _get_redirect_url(self, *args, **kwargs): diff --git a/konova/views/report.py b/konova/views/report.py index 2dca0d15..c98b6ead 100644 --- a/konova/views/report.py +++ b/konova/views/report.py @@ -97,7 +97,7 @@ class BaseReportView(BaseView): """ raise NotImplementedError - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): # Reports do not need specific permissions to be callable return True diff --git a/konova/views/resubmission.py b/konova/views/resubmission.py index 940d09f7..9cf26bd8 100644 --- a/konova/views/resubmission.py +++ b/konova/views/resubmission.py @@ -20,7 +20,7 @@ class AbstractResubmissionView(LoginRequiredMixin, BaseModalFormView): class Meta: abstract = True - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() def _check_for_recorded_instance(self, obj): diff --git a/konova/views/share.py b/konova/views/share.py index 482e62f7..e9757d9f 100644 --- a/konova/views/share.py +++ b/konova/views/share.py @@ -60,7 +60,7 @@ class AbstractShareByTokenView(LoginRequiredMixin, BaseView): ) return redirect("home") - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): # No permissions are needed to get shared access via token return True @@ -77,5 +77,5 @@ class AbstractShareFormView(LoginRequiredMixin, BaseModalFormView): class Meta: abstract = True - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() diff --git a/konova/views/state.py b/konova/views/state.py index 419c573b..4170aef0 100644 --- a/konova/views/state.py +++ b/konova/views/state.py @@ -23,7 +23,7 @@ class AbstractCompensationStateView(LoginRequiredMixin, BaseModalFormView): class Meta: abstract = True - def _user_has_permission(self, user): + def _user_has_permission(self, user, **kwargs): return user.is_default_user() def _get_redirect_url(self, *args, **kwargs): diff --git a/user/urls.py b/user/urls.py index ce8616fd..7712b837 100644 --- a/user/urls.py +++ b/user/urls.py @@ -11,7 +11,9 @@ from user.autocomplete.share import ShareUserAutocomplete, ShareTeamAutocomplete from user.autocomplete.team import TeamAdminAutocomplete from user.views.api_token import APITokenView, new_api_token_view from user.views.propagate import PropagateUserView -from user.views.views import * +from user.views.teams import TeamIndexView, NewTeamView, TeamDetailModalView, EditTeamView, RemoveTeamView, \ + LeaveTeamView +from user.views.users import UserDetailView, NotificationsView, ContactView app_name = "user" urlpatterns = [ @@ -22,11 +24,11 @@ urlpatterns = [ path("token/api/new", new_api_token_view, name="api-token-new"), path("contact/", ContactView.as_view(), name="contact"), path("team/", TeamIndexView.as_view(), name="team-index"), - path("team/new", new_team_view, name="team-new"), + path("team/new", NewTeamView.as_view(), name="team-new"), path("team/", TeamDetailModalView.as_view(), name="team-data"), - path("team//edit", edit_team_view, name="team-edit"), - path("team//remove", remove_team_view, name="team-remove"), - path("team//leave", leave_team_view, name="team-leave"), + path("team//edit", EditTeamView.as_view(), name="team-edit"), + path("team//remove", RemoveTeamView.as_view(), name="team-remove"), + path("team//leave", LeaveTeamView.as_view(), name="team-leave"), # Autocomplete urls path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"), diff --git a/user/views/teams.py b/user/views/teams.py new file mode 100644 index 00000000..bde72b93 --- /dev/null +++ b/user/views/teams.py @@ -0,0 +1,105 @@ +""" +Author: Michel Peltriaux +Created on: 05.11.25 + +""" +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import Http404, HttpRequest +from django.shortcuts import get_object_or_404, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from konova.contexts import BaseContext +from konova.utils.message_templates import TEAM_LEFT, TEAM_REMOVED, TEAM_EDITED, TEAM_ADDED +from konova.views.base import BaseModalFormView +from user.forms.modals.team import LeaveTeamModalForm, RemoveTeamModalForm, EditTeamModalForm, NewTeamModalForm +from user.forms.team import TeamDataForm +from user.models import Team +from user.views.users import UserBaseView + + +class TeamDetailModalView(LoginRequiredMixin, BaseModalFormView): + _FORM_CLS = TeamDataForm + _MODEL_CLS = Team + + def _user_has_shared_access(self, user, **kwargs): + # No specific constraints + return True + + def _user_has_permission(self, user, **kwargs): + # No specific constraints + return True + + +class TeamIndexView(LoginRequiredMixin, UserBaseView): + _TEMPLATE = "user/team/index.html" + _TAB_TITLE = _("Teams") + + def get(self, request: HttpRequest): + user = request.user + context = { + "teams": user.shared_teams, + "tab_title": self._TAB_TITLE, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + +class BaseTeamView(LoginRequiredMixin, BaseModalFormView): + _REDIRECT_URL = "user:team-index" + _MODEL_CLS = Team + + class Meta: + abstract = True + + def _user_has_permission(self, user, **kwargs): + # Nothing to check here - just pass the test + return True + + def _user_has_shared_access(self, user, **kwargs): + # Nothing to check here - just pass the test + return True + + def _get_redirect_url(self, *args, **kwargs): + return reverse(self._REDIRECT_URL) + +class NewTeamView(BaseTeamView): + _FORM_CLS = NewTeamModalForm + _MSG_SUCCESS = TEAM_ADDED + +class EditTeamView(BaseTeamView): + _FORM_CLS = EditTeamModalForm + _MSG_SUCCESS = TEAM_EDITED + + def _user_has_permission(self, user, **kwargs): + team = get_object_or_404(Team, id=kwargs.get("id")) + user_is_admin = team.is_user_admin(user) + if not user_is_admin: + # If user is not an admin, we act as if there is no such team on the database + raise Http404() + return user_is_admin + + +class RemoveTeamView(BaseTeamView): + _FORM_CLS = RemoveTeamModalForm + _MSG_SUCCESS = TEAM_REMOVED + + def _user_has_permission(self, user, **kwargs): + team_id = kwargs.get("id") + team = get_object_or_404(Team, id=team_id) + user_is_admin = team.is_user_admin(user) + if not user_is_admin: + raise Http404() + return True + +class LeaveTeamView(BaseTeamView): + _FORM_CLS = LeaveTeamModalForm + _MSG_SUCCESS = TEAM_LEFT + + def _user_has_shared_access(self, user, **kwargs): + team_id = kwargs.get("id") + team = get_object_or_404(self._MODEL_CLS, id=team_id) + is_user_team_member = team.users.filter(id=user.id).exists() + if not is_user_team_member: + raise Http404() + return True diff --git a/user/views/users.py b/user/views/users.py new file mode 100644 index 00000000..186eee47 --- /dev/null +++ b/user/views/users.py @@ -0,0 +1,81 @@ +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin + +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.views.base import BaseView, BaseModalFormView +from user.forms.modals.user import UserContactForm +from user.forms.user import UserNotificationForm +from user.models import User +from django.http import HttpRequest +from django.shortcuts import render, redirect +from django.utils.translation import gettext_lazy as _ + +from konova.contexts import BaseContext + + +class UserBaseView(BaseView): + def _user_has_shared_access(self, user, **kwargs): + return True + + def _user_has_permission(self, user, **kwargs): + return True + + +class UserDetailView(LoginRequiredMixin, UserBaseView): + _TEMPLATE = "user/index.html" + _TAB_TITLE = _("User settings") + + def get(self, request: HttpRequest): + context = { + "user": request.user, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + +class NotificationsView(LoginRequiredMixin, UserBaseView): + _TEMPLATE = "user/notifications.html" + _TAB_TITLE = _("User notifications") + + def get(self, request: HttpRequest): + user = request.user + form = UserNotificationForm(user=user, data=None) + context = { + "user": user, + "form": form, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + def post(self, request: HttpRequest): + user = request.user + form = UserNotificationForm(user=user, data=request.POST) + if form.is_valid(): + form.save() + messages.success( + request, + _("Notifications edited") + ) + return redirect("user:detail") + context = { + "user": user, + "form": form, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + +class ContactView(LoginRequiredMixin, BaseModalFormView): + _FORM_CLS = UserContactForm + _MODEL_CLS = User + + def _user_has_shared_access(self, user, **kwargs): + # No specific constraints + return True + + def _user_has_permission(self, user, **kwargs): + # No specific constraints + return True diff --git a/user/views/views.py b/user/views/views.py deleted file mode 100644 index 953c6fe3..00000000 --- a/user/views/views.py +++ /dev/null @@ -1,206 +0,0 @@ -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.contrib.auth.mixins import LoginRequiredMixin -from django.urls import reverse - -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.views.base import BaseView, BaseModalFormView -from user.forms.modals.team import NewTeamModalForm, EditTeamModalForm, RemoveTeamModalForm, LeaveTeamModalForm -from user.forms.modals.user import UserContactForm -from user.forms.team import TeamDataForm -from user.forms.user import UserNotificationForm -from user.models import User, Team -from django.http import HttpRequest, Http404 -from django.shortcuts import render, redirect, get_object_or_404 -from django.utils.translation import gettext_lazy as _ - -from konova.contexts import BaseContext -from konova.decorators import login_required_modal - - -class UserBaseView(BaseView): - def _user_has_shared_access(self, user, **kwargs): - return True - - def _user_has_permission(self, user): - return True - - -class UserDetailView(LoginRequiredMixin, UserBaseView): - _TEMPLATE = "user/index.html" - _TAB_TITLE = _("User settings") - - def get(self, request: HttpRequest): - context = { - "user": request.user, - TAB_TITLE_IDENTIFIER: self._TAB_TITLE, - } - context = BaseContext(request, context).context - return render(request, self._TEMPLATE, context) - - -class NotificationsView(LoginRequiredMixin, UserBaseView): - _TEMPLATE = "user/notifications.html" - _TAB_TITLE = _("User notifications") - - def get(self, request: HttpRequest): - user = request.user - form = UserNotificationForm(user=user, data=None) - context = { - "user": user, - "form": form, - TAB_TITLE_IDENTIFIER: self._TAB_TITLE, - } - context = BaseContext(request, context).context - return render(request, self._TEMPLATE, context) - - def post(self, request: HttpRequest): - user = request.user - form = UserNotificationForm(user=user, data=request.POST) - if form.is_valid(): - form.save() - messages.success( - request, - _("Notifications edited") - ) - return redirect("user:detail") - context = { - "user": user, - "form": form, - TAB_TITLE_IDENTIFIER: self._TAB_TITLE, - } - context = BaseContext(request, context).context - return render(request, self._TEMPLATE, context) - - -class ContactView(LoginRequiredMixin, BaseModalFormView): - def get(self, request: HttpRequest, id: str): - """ Renders contact modal view of a users contact data - - Args: - request (HttpRequest): The incoming request - id (str): The user's id - - Returns: - - """ - user = get_object_or_404(User, id=id) - form = UserContactForm(request.POST or None, instance=user, request=request) - context = { - "form": form, - } - context = BaseContext(request, context).context - return render(request, self._TEMPLATE, context) - - def _user_has_shared_access(self, user, **kwargs): - # No specific constraints - return True - - def _user_has_permission(self, user): - # No specific constraints - return True - - -class TeamDetailModalView(LoginRequiredMixin, BaseModalFormView): - def get(self, request: HttpRequest, id: str): - """ Renders team data - - Args: - request (HttpRequest): The incoming request - id (str): The team's id - - Returns: - - """ - team = get_object_or_404(Team, id=id) - form = TeamDataForm(request.POST or None, instance=team, request=request) - context = { - "form": form, - } - context = BaseContext(request, context).context - return render(request, self._TEMPLATE, context) - - def _user_has_shared_access(self, user, **kwargs): - # No specific constraints - return True - - def _user_has_permission(self, user): - # No specific constraints - return True - - -class TeamIndexView(LoginRequiredMixin, UserBaseView): - _TEMPLATE = "user/team/index.html" - _TAB_TITLE = _("Teams") - - def get(self, request: HttpRequest): - user = request.user - context = { - "teams": user.shared_teams, - "tab_title": self._TAB_TITLE, - } - context = BaseContext(request, context).context - return render(request, self._TEMPLATE, context) - - -@login_required_modal -@login_required -def new_team_view(request: HttpRequest): - form = NewTeamModalForm(request.POST or None, request=request) - return form.process_request( - request, - _("New team added"), - redirect_url=reverse("user:team-index") - ) - - -@login_required_modal -@login_required -def edit_team_view(request: HttpRequest, id: str): - team = get_object_or_404(Team, id=id) - user_is_admin = team.is_user_admin(request.user) - if not user_is_admin: - raise Http404() - form = EditTeamModalForm(request.POST or None, instance=team, request=request) - return form.process_request( - request, - _("Team edited"), - redirect_url=reverse("user:team-index") - ) - - -@login_required_modal -@login_required -def remove_team_view(request: HttpRequest, id: str): - team = get_object_or_404(Team, id=id) - user_is_admin = team.is_user_admin(request.user) - if not user_is_admin: - raise Http404() - form = RemoveTeamModalForm(request.POST or None, instance=team, request=request) - return form.process_request( - request, - _("Team removed"), - redirect_url=reverse("user:team-index") - ) - - -@login_required_modal -@login_required -def leave_team_view(request: HttpRequest, id: str): - team = get_object_or_404(Team, id=id) - user = request.user - - is_user_team_member = team.users.filter(id=user.id).exists() - if not is_user_team_member: - messages.info( - request, - _("You are not a member of this team") - ) - return redirect("user:team-index") - - form = LeaveTeamModalForm(request.POST or None, instance=team, request=request) - return form.process_request( - request, - _("Left Team"), - redirect_url=reverse("user:team-index") - ) -- 2.47.2 From cf6f188ef3e2d913b9395269f999f617749e9c0e Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 5 Nov 2025 10:37:27 +0100 Subject: [PATCH 36/36] # Refactoring APITokenView * refactors API Token view * updates tests --- user/forms/modals/api_token.py | 1 + user/tests/unit/test_forms.py | 2 +- user/urls.py | 4 ++-- user/views/api_token.py | 32 ++++++++++++++------------------ 4 files changed, 18 insertions(+), 21 deletions(-) diff --git a/user/forms/modals/api_token.py b/user/forms/modals/api_token.py index d5ead620..5db9f3fb 100644 --- a/user/forms/modals/api_token.py +++ b/user/forms/modals/api_token.py @@ -22,6 +22,7 @@ class NewAPITokenModalForm(BaseModalForm): def __init__(self, *args, **kwargs): self.template = "modal/modal_form.html" super().__init__(*args, **kwargs) + self.instance = self.user self.form_title = _("Generate API Token") self.form_caption = "" diff --git a/user/tests/unit/test_forms.py b/user/tests/unit/test_forms.py index 4a86945f..e036743c 100644 --- a/user/tests/unit/test_forms.py +++ b/user/tests/unit/test_forms.py @@ -260,7 +260,7 @@ class ApiTokenFormTestCase(BaseTestCase): } self.assertIsNone(self.user.api_token) - form = NewAPITokenModalForm(request.POST, instance=self.user) + form = NewAPITokenModalForm(request.POST, request=request) form.save() self.user.refresh_from_db() token = self.user.api_token diff --git a/user/urls.py b/user/urls.py index 7712b837..14ca7ce5 100644 --- a/user/urls.py +++ b/user/urls.py @@ -9,7 +9,7 @@ from django.urls import path from user.autocomplete.share import ShareUserAutocomplete, ShareTeamAutocomplete from user.autocomplete.team import TeamAdminAutocomplete -from user.views.api_token import APITokenView, new_api_token_view +from user.views.api_token import APITokenView, NewAPITokenView from user.views.propagate import PropagateUserView from user.views.teams import TeamIndexView, NewTeamView, TeamDetailModalView, EditTeamView, RemoveTeamView, \ LeaveTeamView @@ -21,7 +21,7 @@ urlpatterns = [ path("propagate/", PropagateUserView.as_view(), name="propagate"), path("notifications/", NotificationsView.as_view(), name="notifications"), path("token/api", APITokenView.as_view(), name="api-token"), - path("token/api/new", new_api_token_view, name="api-token-new"), + path("token/api/new", NewAPITokenView.as_view(), name="api-token-new"), path("contact/", ContactView.as_view(), name="contact"), path("team/", TeamIndexView.as_view(), name="team-index"), path("team/new", NewTeamView.as_view(), name="team-new"), diff --git a/user/views/api_token.py b/user/views/api_token.py index 3b653c28..f90729ab 100644 --- a/user/views/api_token.py +++ b/user/views/api_token.py @@ -4,9 +4,9 @@ Created on: 08.01.25 """ from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest from django.shortcuts import render -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 _ @@ -15,7 +15,9 @@ from konova.contexts import BaseContext from konova.decorators import default_group_required from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import NEW_API_TOKEN_GENERATED +from konova.views.base import BaseModalFormView from user.forms.modals.api_token import NewAPITokenModalForm +from user.models import User class APITokenView(View): @@ -36,22 +38,16 @@ class APITokenView(View): context = BaseContext(request, context).context return render(request, template, context) +class NewAPITokenView(LoginRequiredMixin, BaseModalFormView): + _MODEL_CLS = User + _FORM_CLS = NewAPITokenModalForm + _MSG_SUCCESS = NEW_API_TOKEN_GENERATED + _REDIRECT_URL = "user:api-token" -def new_api_token_view(request: HttpRequest): - """ Function based view for processing ModalForm - (Currently ModalForms only work properly with function based views) + def _user_has_shared_access(self, user, **kwargs): + # No special checks to be done in here + return True - Args: - request (): - - Returns: - - """ - user = request.user - - form = NewAPITokenModalForm(request.POST or None, instance=user, request=request) - return form.process_request( - request=request, - msg_success=NEW_API_TOKEN_GENERATED, - redirect_url=reverse("user:api-token"), - ) \ No newline at end of file + def _user_has_permission(self, user, **kwargs): + # User should at least be a default user to be able to use the api + return user.is_default_user() -- 2.47.2