From 242730435e10796988030200a88368035153e68d Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 16 Oct 2025 15:36:57 +0200 Subject: [PATCH] # 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")