diff --git a/codelist/views.py b/codelist/views.py deleted file mode 100644 index e69de29b..00000000 diff --git a/compensation/forms/compensation.py b/compensation/forms/compensation.py index bb1532d6..51392a2f 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) + 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.") + ) + return valid + def save(self, user: User, geom_form: SimpleGeomForm): with transaction.atomic(): comp, action = self.__create_comp(user) 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/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/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/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/forms/modals/state.py b/compensation/forms/modals/state.py index 7340c95f..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 333ea947..0c0831b4 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()) @@ -186,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) @@ -205,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) @@ -230,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"))) @@ -261,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) @@ -282,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) @@ -294,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/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/compensation/urls/compensation.py b/compensation/urls/compensation.py index 45a11594..d97565f2 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -10,27 +10,28 @@ 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, \ 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 \ + CompensationIndexView, CompensationIdentifierGeneratorView, CompensationDetailView, \ + NewCompensationFormView, EditCompensationFormView, RemoveCompensationView from compensation.views.compensation.log import CompensationLogView urlpatterns = [ # Main compensation - path("", index_view, name="index"), - path('new/id', new_id_view, name='new-id'), - path('new/', new_view, name='new'), - path('new', new_view, name='new'), - path('', detail_view, name='detail'), + path("", CompensationIndexView.as_view(), name="index"), + path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'), + 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('/remove', remove_view, name='remove'), + path('/edit', EditCompensationFormView.as_view(), name='edit'), + 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'), @@ -43,7 +44,7 @@ urlpatterns = [ path('/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"), path('/deadline//edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'), path('/deadline//remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'), - path('/report', report_view, name='report'), + path('/report', 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 beaae8d9..d3d143f1 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -8,11 +8,11 @@ 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 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 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 @@ -28,15 +28,15 @@ from compensation.views.eco_account.deduction import NewEcoAccountDeductionView, app_name = "acc" urlpatterns = [ - path("", index_view, name="index"), - path('new/', new_view, name='new'), - path('new/id', new_id_view, name='new-id'), - path('', detail_view, name='detail'), + path("", EcoAccountIndexView.as_view(), name="index"), + 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', report_view, name='report'), - path('/edit', edit_view, name='edit'), - path('/remove', remove_view, name='remove'), + path('/report', EcoAccountReportView.as_view(), name='report'), + path('/edit', EditEcoAccountFormView.as_view(), name='edit'), + 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/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/compensation/compensation.py b/compensation/views/compensation/compensation.py index 15bac1f8..098849d8 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 @@ -18,300 +18,157 @@ 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, any_group_check, login_required_modal, \ - uuid_required -from konova.forms import SimpleGeomForm +from konova.decorators import shared_access_required, default_group_required, login_required_modal 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 + RECORDED_BLOCKS_EDIT, PARAMS_INVALID +from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ + BaseEditSpatialLocatedObjectFormView +from konova.views.detail import BaseDetailView +from konova.views.remove import BaseRemoveModalFormView -@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 -@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 + 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: + intervention = get_object_or_404(Intervention, id=intervention_id) + return intervention.is_shared_with(user) - Returns: + def _user_has_permission(self, user): + # User has to be an ets user + return user.is_default_user() - """ - 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 + 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 ) - ) - 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) - 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) + 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) -@login_required -@default_group_required -def new_id_view(request: HttpRequest): - """ JSON endpoint +class EditCompensationFormView(BaseEditSpatialLocatedObjectFormView): + _MODEL_CLS = Compensation + _FORM_CLS = EditCompensationForm + _TEMPLATE = "compensation/form/view.html" + _REDIRECT_URL = "compensation:detail" - Provides fetching of free identifiers for e.g. AJAX calls + def _user_has_permission(self, user): + # User has to be a default user + return user.is_default_user() - """ - tmp = Compensation() - identifier = tmp.generate_new_identifier() - while Compensation.objects.filter(identifier=identifier).exists(): - identifier = tmp.generate_new_identifier() - return JsonResponse( - data={ - "gen_data": identifier + +class CompensationIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): + _MODEL_CLS = Compensation + _REDIRECT_URL = "compensation:index" + + +class CompensationDetailView(BaseDetailView): + _MODEL_CLS = Compensation + _TEMPLATE = "compensation/detail/compensation/view.html" + + def _get_object(self, id: str): + """ Returns the compensation + + Args: + id (str): The compensation's id + + 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 + + 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 -@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) - - -@login_required -@any_group_check -@uuid_required -def detail_view(request: HttpRequest, id: str): - """ Renders a detail view for a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - - Returns: - - """ - template = "compensation/detail/compensation/view.html" - comp = get_object_or_404( - Compensation.objects.select_related( - "modified", - "created", - "geometry" - ), - id=id, - deleted=None, - intervention__deleted=None, - ) - geom_form = SimpleGeomForm(instance=comp) - parcels = comp.get_underlying_parcels() - _user = request.user - is_data_shared = comp.intervention.is_shared_with(_user) - - # Order states according to surface - before_states = comp.before_states.all().prefetch_related("biotope_type").order_by("-surface") - after_states = comp.after_states.all().prefetch_related("biotope_type").order_by("-surface") - actions = comp.actions.all().prefetch_related("action_type") - - # Precalculate logical errors between before- and after-states - # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling - sum_before_states = comp.get_surface_before_states() - sum_after_states = comp.get_surface_after_states() - diff_states = abs(sum_before_states - sum_after_states) - - request = comp.set_status_messages(request) - - last_checked = comp.intervention.get_last_checked_action() - last_checked_tooltip = "" - if last_checked: - last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user) - - requesting_user_is_only_shared_user = comp.is_only_shared_with(_user) - if requesting_user_is_only_shared_user: - messages.info( - request, - DO_NOT_FORGET_TO_SHARE - ) - - context = { - "obj": comp, - "last_checked": last_checked, - "last_checked_tooltip": last_checked_tooltip, - "geom_form": geom_form, - "parcels": parcels, - "is_entry_shared": is_data_shared, - "actions": actions, - "before_states": before_states, - "after_states": after_states, - "sum_before_states": sum_before_states, - "sum_after_states": sum_after_states, - "diff_states": diff_states, - "is_default_member": _user.in_group(DEFAULT_GROUP), - "is_zb_member": _user.in_group(ZB_GROUP), - "is_ets_member": _user.in_group(ETS_GROUP), - "LANIS_LINK": comp.get_LANIS_link(), - TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}", - "has_finished_deadlines": comp.get_finished_deadlines().exists(), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required_modal -@login_required -@default_group_required -@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/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/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/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/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/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/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/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/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/compensation/views/eco_account/deduction.py b/compensation/views/eco_account/deduction.py index 1de6c605..0dddc1eb 100644 --- a/compensation/views/eco_account/deduction.py +++ b/compensation/views/eco_account/deduction.py @@ -5,54 +5,33 @@ 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 +_ECO_ACCOUNT_DETAIl_URL_NAME = "compensation:acc:detail" -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_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 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) + def _check_for_recorded_instance(self, obj): + # Deductions can be created on recorded as well as on non-recorded entries + return None -class RemoveEcoAccountDeductionView(AbstractRemoveDeductionView): - def _custom_check(self, obj): - pass +class EditEcoAccountDeductionView(LoginRequiredMixin, AbstractEditDeductionView): + _MODEL_CLS = EcoAccount + _REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME - 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_CLS = EcoAccount + _REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME 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/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 28dbfc10..f9c50ab8 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.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.db.models import Sum -from django.http import HttpRequest, JsonResponse +from django.contrib.auth.mixins import LoginRequiredMixin +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,43 +17,52 @@ 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, BaseNewSpatialLocatedObjectFormView, \ + BaseEditSpatialLocatedObjectFormView +from konova.views.detail import BaseDetailView +from konova.views.remove import BaseRemoveModalFormView -@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 + def _get_queryset(self): + qs = EcoAccount.objects.filter( + deleted=None, + ).order_by( + "-modified__timestamp" + ) + return qs - 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) + +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 @@ -112,23 +121,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 = "compensation:acc:index" @login_required @@ -192,116 +187,78 @@ 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 -@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/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/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/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/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/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/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/forms.py b/ema/forms.py index 26bbc2db..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 +from konova.forms.modals import NewDocumentModalForm, EditDocumentModalForm, RemoveDocumentModalForm, \ + ResubmissionModalForm from user.models import UserActionLogEntry @@ -170,4 +171,13 @@ 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 + +class EmaResubmissionModalForm(ResubmissionModalForm): + _MODEL_CLS = Ema diff --git a/ema/urls.py b/ema/urls.py index bff7c41d..835530bb 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -10,25 +10,26 @@ 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 EmaIndexView, EmaIdentifierGeneratorView, EmaDetailView, EditEmaFormView, NewEmaFormView, \ + RemoveEmaView 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 app_name = "ema" urlpatterns = [ - path("", index_view, name="index"), - path("new/", new_view, name="new"), - path("new/id", new_id_view, name="new-id"), - path("", detail_view, name="detail"), + path("", EmaIndexView.as_view(), name="index"), + 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('/remove', remove_view, name='remove'), + path('/edit', EditEmaFormView.as_view(), name='edit'), + path('/remove', RemoveEmaView.as_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/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/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/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/ema/views/ema.py b/ema/views/ema.py index 5322b5ef..6057a95c 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -5,269 +5,112 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.db.models import Sum -from django.http import HttpRequest, JsonResponse -from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse +from django.contrib.auth.mixins import LoginRequiredMixin +from django.shortcuts import get_object_or_404 from django.utils.translation import gettext_lazy as _ from ema.forms import NewEmaForm, EditEmaForm from ema.models import Ema from ema.tables import EmaTable -from konova.contexts import BaseContext -from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal, \ - uuid_required -from konova.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 +from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ + BaseEditSpatialLocatedObjectFormView +from konova.views.detail import BaseDetailView +from konova.views.remove import BaseRemoveModalFormView -@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 -@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 - - Returns: - - """ - template = "ema/form/view.html" - data_form = NewEmaForm(request.POST or None) - geom_form = SimpleGeomForm(request.POST or None, read_only=False) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - generated_identifier = data_form.cleaned_data.get("identifier", None) - ema = data_form.save(request.user, geom_form) - if generated_identifier != ema.identifier: - messages.info( - request, - IDENTIFIER_REPLACED.format( - generated_identifier, - ema.identifier - ) - ) - messages.success(request, _("EMA {} added").format(ema.identifier)) - 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) + def _user_has_permission(self, user): + # User has to be an ets user + return user.is_ets_user() -@login_required -@conservation_office_group_required -def new_id_view(request: HttpRequest): - """ JSON endpoint +class EditEmaFormView(BaseEditSpatialLocatedObjectFormView): + _MODEL_CLS = Ema + _FORM_CLS = EditEmaForm + _TEMPLATE = "ema/form/view.html" + _REDIRECT_URL = "ema:detail" + _TAB_TITLE = _("Edit {}") - Provides fetching of free identifiers for e.g. AJAX calls + def _user_has_permission(self, user): + # User has to be an ets user + return user.is_ets_user() - """ - tmp = Ema() - identifier = tmp.generate_new_identifier() - while Ema.objects.filter(identifier=identifier).exists(): - identifier = tmp.generate_new_identifier() - return JsonResponse( - data={ - "gen_data": identifier + +class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): + _MODEL_CLS = Ema + _REDIRECT_URL = "ema:index" + + def _user_has_permission(self, user): + return user.is_ets_user() + + +class EmaDetailView(BaseDetailView): + _MODEL_CLS = Ema + _TEMPLATE = "ema/detail/view.html" + + def _get_object(self, id: str): + """ Fetch object for detail view + + Args: + id (str): The record's id' + + Returns: + + """ + ema = get_object_or_404(Ema, id=id, deleted=None) + return ema + + def _get_detail_context(self, obj: Ema): + """ Generate object specific detail context for view + + Args: + obj (): The record + + Returns: + + """ + # Order states according to surface + before_states = obj.before_states.all().order_by("-surface") + after_states = obj.after_states.all().order_by("-surface") + + # Precalculate logical errors between before- and after-states + # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling + sum_before_states = 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 +class RemoveEmaView(LoginRequiredMixin, BaseRemoveModalFormView): + _MODEL_CLS = Ema + _REDIRECT_URL = "ema:index" -@login_required -@uuid_required -def detail_view(request: HttpRequest, id: str): - """ Renders the detail view of an EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA id - - Returns: - - """ - template = "ema/detail/view.html" - ema = get_object_or_404(Ema, id=id, deleted=None) - - geom_form = SimpleGeomForm(instance=ema) - parcels = ema.get_underlying_parcels() - _user = request.user - is_entry_shared = ema.is_shared_with(_user) - - # Order states according to surface - before_states = ema.before_states.all().order_by("-surface") - after_states = ema.after_states.all().order_by("-surface") - - # Precalculate logical errors between before- and after-states - # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling - sum_before_states = ema.get_surface_before_states() - sum_after_states = ema.get_surface_after_states() - diff_states = abs(sum_before_states - sum_after_states) - - ema.set_status_messages(request) - - requesting_user_is_only_shared_user = ema.is_only_shared_with(_user) - if requesting_user_is_only_shared_user: - messages.info( - request, - DO_NOT_FORGET_TO_SHARE - ) - - context = { - "obj": ema, - "geom_form": geom_form, - "parcels": parcels, - "is_entry_shared": is_entry_shared, - "before_states": before_states, - "after_states": after_states, - "sum_before_states": sum_before_states, - "sum_after_states": sum_after_states, - "diff_states": diff_states, - "is_default_member": _user.in_group(DEFAULT_GROUP), - "is_zb_member": _user.in_group(ZB_GROUP), - "is_ets_member": _user.in_group(ETS_GROUP), - "LANIS_LINK": ema.get_LANIS_link(), - TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}", - "has_finished_deadlines": ema.get_finished_deadlines().exists(), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -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 -@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/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/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/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/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/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/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/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/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/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/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 8a148197..10a6e5f0 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -8,35 +8,36 @@ 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 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 InterventionIndexView, InterventionIdentifierGeneratorView, \ + InterventionDetailView, NewInterventionFormView, EditInterventionFormView, RemoveInterventionView 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 +from intervention.views.revocation import NewRevocationView, GetRevocationDocumentView, EditRevocationView, \ + RemoveRevocationView from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView app_name = "intervention" urlpatterns = [ - path("", index_view, name="index"), - path('new/', new_view, name='new'), - path('new/id', new_id_view, name='new-id'), - path('', detail_view, name='detail'), + path("", InterventionIndexView.as_view(), name="index"), + 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'), - path('/edit', edit_view, name='edit'), - path('/remove', remove_view, name='remove'), + path('/edit', EditInterventionFormView.as_view(), name='edit'), + path('/remove', RemoveInterventionView.as_view(), name='remove'), path('/share/', InterventionShareByTokenView.as_view(), name='share-token'), path('/share', InterventionShareFormView.as_view(), name='share-form'), - path('/check', check_view, name='check'), + path('/check', InterventionCheckView.as_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 @@ -54,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/check.py b/intervention/views/check.py index 1fae75bb..07387913 100644 --- a/intervention/views/check.py +++ b/intervention/views/check.py @@ -5,35 +5,24 @@ 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") + _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 962fe807..8aed81bf 100644 --- a/intervention/views/deduction.py +++ b/intervention/views/deduction.py @@ -5,51 +5,27 @@ 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.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(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_CLS = Intervention + _MSG_SUCCESS = DEDUCTION_ADDED + _REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME -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_CLS = Intervention + _MSG_SUCCESS = DEDUCTION_EDITED + _REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME -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_CLS = Intervention + _MSG_SUCCESS = DEDUCTION_REMOVED + _REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME 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/intervention/views/intervention.py b/intervention/views/intervention.py index 0d6cc369..1350340a 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -7,207 +7,115 @@ Created on: 19.08.22 """ from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.http import JsonResponse, HttpRequest +from django.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, any_group_check, login_required_modal, \ - uuid_required +from konova.decorators import default_group_required, shared_access_required from konova.forms import SimpleGeomForm -from konova.forms.modals import RemoveModalForm -from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ - CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \ - GEOMETRIES_IGNORED_TEMPLATE + 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 -@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 -@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") -@login_required -@default_group_required -def new_id_view(request: HttpRequest): - """ JSON endpoint +class EditInterventionFormView(BaseEditSpatialLocatedObjectFormView): + _MODEL_CLS = Intervention + _FORM_CLS = EditInterventionForm + _TEMPLATE = "intervention/form/view.html" + _REDIRECT_URL = "intervention:detail" + _TAB_TITLE = _("Edit {}") - Provides fetching of free identifiers for e.g. AJAX calls - """ - tmp_intervention = Intervention() - identifier = tmp_intervention.generate_new_identifier() - while Intervention.objects.filter(identifier=identifier).exists(): - identifier = tmp_intervention.generate_new_identifier() - return JsonResponse( - data={ - "gen_data": identifier +class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): + _MODEL_CLS = Intervention + _REDIRECT_URL = "intervention:index" + + +class InterventionDetailView(BaseDetailView): + _MODEL_CLS = Intervention + _TEMPLATE = "intervention/detail/view.html" + + def _get_object(self, id: str): + """ Returns the intervention + + Args: + id (str): The intervention's id + + 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 + + def _get_detail_context(self, obj: Intervention): + """ Generate object specific detail context for view + + Args: + obj (): The record + + Returns: + + """ + 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 + ) + + 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, } - ) - - -@login_required -@any_group_check -@uuid_required -def detail_view(request: HttpRequest, id: str): - """ Renders a detail view for viewing an intervention's data - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - - Returns: - - """ - template = "intervention/detail/view.html" - - # Fetch data, filter out deleted related data - intervention = get_object_or_404( - Intervention.objects.select_related( - "geometry", - "legal", - "responsible", - ).prefetch_related( - "legal__revocations", - ), - id=id, - deleted=None - ) - compensations = intervention.compensations.filter( - deleted=None, - ) - _user = request.user - is_data_shared = intervention.is_shared_with(user=_user) - - geom_form = SimpleGeomForm( - instance=intervention, - ) - last_checked = intervention.get_last_checked_action() - last_checked_tooltip = "" - if last_checked: - last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format( - last_checked.get_timestamp_str_formatted(), - last_checked.user - ) - - has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists() - - requesting_user_is_only_shared_user = intervention.is_only_shared_with(_user) - if requesting_user_is_only_shared_user: - messages.info( - request, - DO_NOT_FORGET_TO_SHARE - ) - - context = { - "obj": intervention, - "last_checked": last_checked, - "last_checked_tooltip": last_checked_tooltip, - "compensations": compensations, - "is_entry_shared": is_data_shared, - "geom_form": geom_form, - "is_default_member": _user.in_group(DEFAULT_GROUP), - "is_zb_member": _user.in_group(ZB_GROUP), - "is_ets_member": _user.in_group(ETS_GROUP), - "LANIS_LINK": intervention.get_LANIS_link(), - "has_payment_without_document": has_payment_without_document, - TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}", - } - - request = intervention.set_status_messages(request) - - context = BaseContext(request, context).context - return render(request, template, context) + return context @login_required @@ -272,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/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/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/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/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/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" 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/forms/base_form.py b/konova/forms/base_form.py index 98eaa41c..2e1cc960 100644 --- a/konova/forms/base_form.py +++ b/konova/forms/base_form.py @@ -10,21 +10,18 @@ from abc import abstractmethod from django import forms from django.utils.translation import gettext_lazy as _ -from compensation.models import EcoAccount -from konova.models import BaseObject - class BaseForm(forms.Form): """ Basic form for that holds attributes needed in all other forms """ - template = None action_url = None action_btn_label = _("Save") form_title = None cancel_redirect = None form_caption = None instance = None # The data holding model object + 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 +30,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 @@ -42,11 +40,10 @@ class BaseForm(forms.Form): self.has_required_fields = True break - self.check_for_recorded_instance() self.__check_valid_label_input_ratio() @abstractmethod - def save(self): + def save(self, *arg, **kwargs): # To be implemented in subclasses! pass @@ -136,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/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 f09af99a..d4c36f81 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,9 +52,19 @@ 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): - 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/static/css/konova.css b/konova/static/css/konova.css index 0c872967..8eb5f447 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; +} + +.netgis-menu{ + z-index: 100 !important; } \ No newline at end of file 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/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..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) @@ -256,7 +255,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 +272,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() 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/utils/general.py b/konova/utils/general.py index b7ee0ea8..dd7dc1ea 100644 --- a/konova/utils/general.py +++ b/konova/utils/general.py @@ -5,6 +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, Http404 def format_german_float(num) -> str: @@ -19,3 +24,27 @@ 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 + +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 + UUID(uuid) + except ValueError: + raise Http404 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/message_templates.py b/konova/utils/message_templates.py index ba44934b..a7e05c53 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -19,7 +19,14 @@ 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") + +# 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") +ENTRY_UNRECORDED = _("{} unrecorded") # SHARE DATA_UNSHARED = _("This data is not shared with you") @@ -94,4 +101,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/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/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/base.py b/konova/views/base.py new file mode 100644 index 00000000..5f9db037 --- /dev/null +++ b/konova/views/base.py @@ -0,0 +1,424 @@ +""" +Author: Michel Peltriaux +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, HttpResponseRedirect +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.models import BaseObject +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, FORM_INVALID + + +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): + 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) + + @abstractmethod + def _user_has_permission(self, user): + """ Has to be implemented properly by inheriting classes + + Args: + user (): + + Returns: + + """ + raise NotImplementedError("User permission not checked!") + + @abstractmethod + def _user_has_shared_access(self, user, **kwargs): + """ Has to be implemented properly by inheriting classes + + Args: + user (): + + Returns: + + """ + raise NotImplementedError("Shared access not checked!") + + 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 + _MSG_SUCCESS = 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) + self._check_for_recorded_instance(obj) + form = self._FORM_CLS( + request.POST or None, + request.FILES 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) + self._check_for_recorded_instance(obj) + 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 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, + 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,)) + + 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 + + """ + _TEMPLATE = "generic_index.html" + _INDEX_TABLE_CLS = None + _REDIRECT_URL = "home" + + class Meta: + abstract = True + + 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 + + def _user_has_permission(self, user): + # No specific permissions needed for opening base index view + return True + + 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: str = "home" + + class Meta: + abstract = True + + 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 + } + ) + + def _user_has_permission(self, user): + """ Should be overwritten in inheriting classes! + + Args: + user (): + + Returns: + + """ + return user.is_default_user() + + 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_additional_context(self, **kwargs): + """ + + Args: + **kwargs (): + + Returns: + + """ + return {} + + +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() + + 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, **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() + 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, **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(): + 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) + + 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(obj_redirect_url) + else: + context = self._get_additional_context() + messages.error(request, FORM_INVALID, extra_tags="danger",) + + 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, + id=id + ) + obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,)) + + if obj.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + 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) + + context = self._get_additional_context() + 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) + + def post(self, request: HttpRequest, id: str): + obj = get_object_or_404( + self._MODEL_CLS, + id=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(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) + messages.success(request, _("{} edited").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(obj_redirect_url) + else: + context = self._get_additional_context() + messages.error(request, FORM_INVALID, extra_tags="danger",) + + 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) + + 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/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() diff --git a/konova/views/deduction.py b/konova/views/deduction.py index 5158f7b0..b7b9968c 100644 --- a/konova/views/deduction.py +++ b/konova/views/deduction.py @@ -6,126 +6,88 @@ Created on: 22.08.22 """ from django.core.exceptions import ObjectDoesNotExist -from django.http import Http404 -from django.shortcuts import get_object_or_404 from django.urls import reverse -from django.views import View from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \ RemoveEcoAccountDeductionModalForm -from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_EDITED, DEDUCTION_REMOVED, DEDUCTION_UNKNOWN +from konova.utils.general import check_id_is_valid_uuid +from konova.views.base import BaseModalFormView -class AbstractDeductionView(View): - model = None - redirect_url = None +class AbstractDeductionView(BaseModalFormView): + _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 """ - 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_CLS.objects.get( + id=kwargs.get("id") + ) + ret_val = obj.is_shared_with(user) + except ObjectDoesNotExist: + 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 _custom_check(self, obj): - pass + 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 _custom_check(self, obj): - pass + 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 new file mode 100644 index 00000000..72b4ad17 --- /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.get('id')) + 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/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 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 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 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() 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") diff --git a/konova/views/record.py b/konova/views/record.py index b80d1b0f..cf780acb 100644 --- a/konova/views/record.py +++ b/konova/views/record.py @@ -5,46 +5,28 @@ 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.utils.message_templates import ENTRY_RECORDED, ENTRY_UNRECORDED +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: + if obj.is_recorded: + return ENTRY_RECORDED.format(obj.identifier) + else: + return ENTRY_UNRECORDED.format(obj.identifier) - """ - 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) + 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/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 diff --git a/konova/views/resubmission.py b/konova/views/resubmission.py index 634949de..940d09f7 100644 --- a/konova/views/resubmission.py +++ b/konova/views/resubmission.py @@ -5,51 +5,24 @@ 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 + def _user_has_permission(self, user): + return user.is_default_user() - 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 _check_for_recorded_instance(self, obj): + # Resubmissions are allowed despite an entry being recorded + return None \ No newline at end of file diff --git a/konova/views/share.py b/konova/views/share.py index abcbecaa..482e62f7 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,22 @@ class AbstractShareByTokenView(View): ) return redirect("home") + def _user_has_permission(self, user): + # No permissions are needed to get shared access via token + return True -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() 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) - diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 4f5c1c6c..c9a22673 100644 Binary files a/locale/de/LC_MESSAGES/django.mo and b/locale/de/LC_MESSAGES/django.mo differ diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 6f308995..d7e46d3b 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -45,7 +45,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2025-10-15 09:11+0200\n" +"POT-Creation-Date: 2025-10-19 13:56+0200\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \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:120 +#: 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" @@ -475,7 +483,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 +1296,45 @@ msgstr "" msgid "Responsible data" msgstr "Daten zu den verantwortlichen Stellen" -#: compensation/views/compensation/compensation.py:58 +#: compensation/views/compensation/compensation.py:35 msgid "Compensations - Overview" msgstr "Kompensationen - Übersicht" -#: compensation/views/compensation/compensation.py:181 +#: 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:196 -#: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:238 -#: intervention/views/intervention.py:253 +#: 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 {}" -#: 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:42 msgid "New EMA" msgstr "Neue EMA hinzufügen" @@ -1353,19 +1362,11 @@ msgstr "" msgid "Payment funded compensation" msgstr "Ersatzzahlungsmaßnahme" -#: ema/views/ema.py:53 +#: ema/views/ema.py:26 msgid "EMAs - Overview" msgstr "EMAs - Übersicht" -#: ema/views/ema.py:86 -msgid "EMA {} added" -msgstr "EMA {} hinzugefügt" - -#: ema/views/ema.py:223 -msgid "EMA {} edited" -msgstr "EMA {} bearbeitet" - -#: ema/views/ema.py:262 +#: ema/views/ema.py:138 msgid "EMA removed" msgstr "EMA entfernt" @@ -1429,7 +1430,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 +1666,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:154 msgid "Intervention {} edited" msgstr "Eingriff {} bearbeitet" -#: intervention/views/intervention.py:278 +#: intervention/views/intervention.py:204 msgid "{} removed" msgstr "{} entfernt" @@ -1689,7 +1686,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 +1798,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 +1807,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 +2265,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 +2308,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:281 +msgid "{} edited" +msgstr "{} bearbeitet" + +#: konova/views/home.py:72 templates/navbars/navbar.html:16 msgid "Home" msgstr "Home" @@ -2330,6 +2336,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 +3066,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 +3126,40 @@ 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" + +#~ msgid "EMA {} added" +#~ msgstr "EMA {} hinzugefügt" + +#~ msgid "EMA {} edited" +#~ msgstr "EMA {} bearbeitet" 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..953c6fe3 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,132 @@ 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 UserBaseView(BaseView): + def _user_has_shared_access(self, user, **kwargs): + return True - 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 _user_has_permission(self, user): + return True -@login_required -@any_group_check -def notifications_view(request: HttpRequest): - """ Renders the notifications settings view +class UserDetailView(LoginRequiredMixin, UserBaseView): + _TEMPLATE = "user/index.html" + _TAB_TITLE = _("User settings") - Args: - request (): + 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) - Returns: - """ - template = "user/notifications.html" - user = request.user +class NotificationsView(LoginRequiredMixin, UserBaseView): + _TEMPLATE = "user/notifications.html" + _TAB_TITLE = _("User notifications") - form = UserNotificationForm(user=user, data=request.POST or None) - if request.method == "POST": + 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: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) + + def _user_has_shared_access(self, user, **kwargs): + # No specific constraints + return True + + def _user_has_permission(self, user): + # No specific constraints + return True -@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) + + def _user_has_shared_access(self, user, **kwargs): + # No specific constraints + return True + + def _user_has_permission(self, user): + # No specific constraints + return True -@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, 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