# 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
This commit is contained in:
mpeltriaux 2025-10-16 15:36:57 +02:00
parent afbdf221c3
commit 242730435e
10 changed files with 148 additions and 114 deletions

View File

View File

@ -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

View File

@ -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"

View File

@ -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

View File

@ -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()

View File

@ -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"

View File

@ -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

View File

@ -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

View File

@ -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):

View File

@ -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")