diff --git a/api/urls/urls.py b/api/urls/urls.py index abe9eacf..fe0ddbbc 100644 --- a/api/urls/urls.py +++ b/api/urls/urls.py @@ -7,11 +7,8 @@ Created on: 21.01.22 """ from django.urls import path, include -from api.views.method_views import generate_new_token_view - app_name = "api" urlpatterns = [ path("v1/", include("api.urls.v1.urls", namespace="v1")), - path("token/generate", generate_new_token_view, name="generate-new-token"), ] \ No newline at end of file diff --git a/api/views/method_views.py b/api/views/method_views.py deleted file mode 100644 index 7c6904a5..00000000 --- a/api/views/method_views.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 27.01.22 - -""" -from django.contrib.auth.decorators import login_required -from django.http import HttpRequest, JsonResponse - -from api.models import APIUserToken - - -@login_required -def generate_new_token_view(request: HttpRequest): - """ Handles request for fetching - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - - if request.method == "GET": - token = APIUserToken() - while APIUserToken.objects.filter(token=token.token).exists(): - token = APIUserToken() - return JsonResponse( - data={ - "gen_data": token.token - } - ) - else: - raise NotImplementedError \ No newline at end of file diff --git a/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/eco_account.py b/compensation/forms/eco_account.py index 42443025..976a1c7f 100644 --- a/compensation/forms/eco_account.py +++ b/compensation/forms/eco_account.py @@ -237,7 +237,11 @@ class EditEcoAccountForm(NewEcoAccountForm): class RemoveEcoAccountModalForm(RemoveModalForm): + """ Form class + Provides a form for deleting eco accounts + + """ def is_valid(self): super_valid = super().is_valid() has_deductions = self.instance.deductions.exists() 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/payment.py b/compensation/forms/modals/payment.py index eed2e702..fa69dd15 100644 --- a/compensation/forms/modals/payment.py +++ b/compensation/forms/modals/payment.py @@ -6,8 +6,10 @@ Created on: 18.08.22 """ from django import forms +from django.shortcuts import get_object_or_404 from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _ +from compensation.models import Payment from konova.forms.modals import RemoveModalForm, BaseModalForm from konova.utils import validators from konova.utils.message_templates import PAYMENT_EDITED @@ -103,7 +105,8 @@ class EditPaymentModalForm(NewPaymentForm): payment = None def __init__(self, *args, **kwargs): - self.payment = kwargs.pop("payment", None) + payment_id = kwargs.pop("payment_id", None) + self.payment = get_object_or_404(Payment, id=payment_id) super().__init__(*args, **kwargs) self.form_title = _("Edit payment") form_date = { @@ -133,8 +136,8 @@ class RemovePaymentModalForm(RemoveModalForm): payment = None def __init__(self, *args, **kwargs): - payment = kwargs.pop("payment", None) - self.payment = payment + payment_id = kwargs.pop("payment_id", None) + self.payment = get_object_or_404(Payment, id=payment_id) super().__init__(*args, **kwargs) def save(self): diff --git a/compensation/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/urls/payment.py b/compensation/urls/payment.py index b51384dd..2c4d39a6 100644 --- a/compensation/urls/payment.py +++ b/compensation/urls/payment.py @@ -6,11 +6,11 @@ Created on: 24.08.21 """ from django.urls import path -from compensation.views.payment import * +from compensation.views.payment import NewPaymentView, RemovePaymentView, EditPaymentView app_name = "pay" urlpatterns = [ - path('/new', new_payment_view, name='new'), - path('/remove/', payment_remove_view, name='remove'), - path('/edit/', payment_edit_view, name='edit'), + path('/new', NewPaymentView.as_view(), name='new'), + path('/remove/', RemovePaymentView.as_view(), name='remove'), + path('/edit/', EditPaymentView.as_view(), name='edit'), ] diff --git a/compensation/views/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..f2bae760 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -6,312 +6,166 @@ 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 +from django.shortcuts import get_object_or_404, redirect from django.utils.translation import gettext_lazy as _ from compensation.forms.compensation import EditCompensationForm, NewCompensationForm 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.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 +from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ + RECORDED_BLOCKS_EDIT, PARAMS_INVALID +from konova.views.identifier import AbstractIdentifierGeneratorView +from konova.views.form import AbstractNewGeometryFormView, AbstractEditGeometryFormView +from konova.views.index import AbstractIndexView +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, AbstractIndexView): + _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(AbstractNewGeometryFormView): + _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, **kwargs): + # 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(AbstractEditGeometryFormView): + _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, **kwargs): + # 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, AbstractIdentifierGeneratorView): + _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, **kwargs): + 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..3d869d14 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 AbstractReportView -@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(AbstractReportView): + 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..5351e5e3 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -5,303 +5,159 @@ 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 compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm, RemoveEcoAccountModalForm 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.forms import SimpleGeomForm -from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \ - IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE +from konova.views.identifier import AbstractIdentifierGeneratorView +from konova.views.form import AbstractNewGeometryFormView, AbstractEditGeometryFormView +from konova.views.index import AbstractIndexView +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 - - Args: - request (HttpRequest): The incoming request - - Returns: - A rendered view - """ - template = "generic_index.html" - eco_accounts = EcoAccount.objects.filter( - deleted=None, - ).order_by( - "-modified__timestamp" - ) - table = EcoAccountTable( - request=request, - queryset=eco_accounts - ) - context = { - "table": table, - TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -def new_view(request: HttpRequest): - """ - Renders a view for a new eco account creation - - Args: - request (HttpRequest): The incoming request - - Returns: +class EcoAccountIndexView(LoginRequiredMixin, AbstractIndexView): + """ View class for indexing eco accounts """ - template = "compensation/form/view.html" - data_form = NewEcoAccountForm(request.POST or None) - geom_form = SimpleGeomForm(request.POST or None, read_only=False) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - generated_identifier = data_form.cleaned_data.get("identifier", None) - acc = data_form.save(request.user, geom_form) - if generated_identifier != acc.identifier: - messages.info( - request, - IDENTIFIER_REPLACED.format( - generated_identifier, - acc.identifier - ) - ) - messages.success(request, _("Eco-Account {} added").format(acc.identifier)) - if geom_form.has_geometry_simplified(): - messages.info( - request, - GEOMETRY_SIMPLIFIED - ) + _INDEX_TABLE_CLS = EcoAccountTable + _TAB_TITLE = _("Eco-account - Overview") - num_ignored_geometries = geom_form.get_num_geometries_ignored() - if num_ignored_geometries > 0: - messages.info( - request, - GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) - ) - - return redirect("compensation:acc:detail", id=acc.id) - else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("New Eco-Account"), - } - context = BaseContext(request, context).context - return render(request, template, context) + def _get_queryset(self): + qs = EcoAccount.objects.filter( + deleted=None, + ).order_by( + "-modified__timestamp" + ) + return qs -@login_required -@default_group_required -def new_id_view(request: HttpRequest): - """ JSON endpoint +class NewEcoAccountFormView(AbstractNewGeometryFormView): + """ Form view class - Provides fetching of free identifiers for e.g. AJAX calls + Renders a form for new eco accounts """ - 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 + _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, **kwargs): + # User has to be a default user + return user.is_default_user() + + +class EditEcoAccountFormView(AbstractEditGeometryFormView): + """ Form view class + + Renders a form for editing of eco accounts + + """ + _FORM_CLS = EditEcoAccountForm + _MODEL_CLS = EcoAccount + _TEMPLATE = "compensation/form/view.html" + _REDIRECT_URL = "compensation:acc:detail" + + def _user_has_permission(self, user, **kwargs): + # User has to be a default user + return user.is_default_user() + + +class EcoAccountIdentifierGeneratorView(LoginRequiredMixin, AbstractIdentifierGeneratorView): + """ View class for identifier generation on eco accounts + + """ + _MODEL_CLS = EcoAccount + _REDIRECT_URL = "compensation:acc:index" + + +class EcoAccountDetailView(BaseDetailView): + """ Detail view class + + Renders details of an eco account + + """ + _MODEL_CLS = EcoAccount + _TEMPLATE = "compensation/detail/eco_account/view.html" + + def _get_object(self, id: str): + """ Fetch object for detail view + + Args: + id (str): The record's id' + + Returns: + + """ + acc = get_object_or_404( + EcoAccount.objects.prefetch_related( + "deadlines", + ).select_related( + 'geometry', + 'responsible', + ), + id=id, + deleted=None, + ) + return acc + + 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 -@default_group_required -@shared_access_required(EcoAccount, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing compensations +class RemoveEcoAccountView(LoginRequiredMixin, BaseRemoveModalFormView): + """ Form view class - Args: - request (HttpRequest): The incoming request - - Returns: + Renders a form for removing eco accounts """ - template = "compensation/form/view.html" - # Get object from db - acc = get_object_or_404(EcoAccount, id=id) - if acc.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("compensation:acc:detail", id=id) - - # Create forms, initialize with values from db/from POST request - data_form = EditEcoAccountForm(request.POST or None, instance=acc) - geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc) - if request.method == "POST": - data_form_valid = data_form.is_valid() - geom_form_valid = geom_form.is_valid() - if data_form_valid and geom_form_valid: - # The data form takes the geom form for processing, as well as the performing user - acc = data_form.save(request.user, geom_form) - messages.success(request, _("Eco-Account {} edited").format(acc.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:acc:detail", id=acc.id) - else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@any_group_check -@uuid_required -def detail_view(request: HttpRequest, id: str): - """ Renders a detail view for a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - - Returns: - - """ - template = "compensation/detail/eco_account/view.html" - acc = get_object_or_404( - EcoAccount.objects.prefetch_related( - "deadlines", - ).select_related( - 'geometry', - 'responsible', - ), - id=id, - deleted=None, - ) - geom_form = SimpleGeomForm(instance=acc) - parcels = acc.get_underlying_parcels() - _user = request.user - is_data_shared = acc.is_shared_with(_user) - - # Order states according to surface - before_states = acc.before_states.order_by("-surface") - after_states = acc.after_states.order_by("-surface") - - # Precalculate logical errors between before- and after-states - # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling - sum_before_states = acc.get_surface_before_states() - sum_after_states = acc.get_surface_after_states() - diff_states = abs(sum_before_states - sum_after_states) - # Calculate rest of available surface for deductions - available_total = acc.deductable_rest - available_relative = acc.get_deductable_rest_relative() - - # Prefetch related data to decrease the amount of db connections - deductions = acc.deductions.filter( - intervention__deleted=None, - ) - actions = acc.actions.all() - - request = acc.set_status_messages(request) - - requesting_user_is_only_shared_user = acc.is_only_shared_with(_user) - if requesting_user_is_only_shared_user: - messages.info( - request, - DO_NOT_FORGET_TO_SHARE - ) - - context = { - "obj": acc, - "geom_form": geom_form, - "parcels": parcels, - "is_entry_shared": is_data_shared, - "before_states": before_states, - "after_states": after_states, - "sum_before_states": sum_before_states, - "sum_after_states": sum_after_states, - "diff_states": diff_states, - "available": available_relative, - "available_total": available_total, - "is_default_member": _user.in_group(DEFAULT_GROUP), - "is_zb_member": _user.in_group(ZB_GROUP), - "is_ets_member": _user.in_group(ETS_GROUP), - "LANIS_LINK": acc.get_LANIS_link(), - "deductions": deductions, - "actions": actions, - TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}", - "has_finished_deadlines": acc.get_finished_deadlines().exists(), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required_modal -@login_required -@default_group_required -@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"), - ) + _MODEL_CLS = EcoAccount + _FORM_CLS = RemoveEcoAccountModalForm + _REDIRECT_URL = "compensation:acc:index" + def _user_has_permission(self, user, **kwargs): + 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/compensation/views/payment.py b/compensation/views/payment.py index 29e89ac3..a6a35637 100644 --- a/compensation/views/payment.py +++ b/compensation/views/payment.py @@ -5,84 +5,38 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 09.08.21 """ -from django.urls import reverse -from django.contrib.auth.decorators import login_required -from django.http import HttpRequest -from django.shortcuts import get_object_or_404 +from django.contrib.auth.mixins import LoginRequiredMixin from compensation.forms.modals.payment import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm -from compensation.models import Payment from intervention.models import Intervention -from konova.decorators import default_group_required, shared_access_required from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED +from konova.views.modal import AbstractModalFormView -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def new_payment_view(request: HttpRequest, id: str): - """ Renders a modal view for adding new payments +class BasePaymentView(LoginRequiredMixin, AbstractModalFormView): + _MODEL_CLS = Intervention + _REDIRECT_URL = "intervention:detail" - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id for which a new payment shall be added + class Meta: + abstract = True - Returns: + def _get_redirect_url(self, *args, **kwargs): + url = super()._get_redirect_url(*args, **kwargs) + return f"{url}#related_data" - """ - intervention = get_object_or_404(Intervention, id=id) - form = NewPaymentForm(request.POST or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=PAYMENT_ADDED, - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" - ) + def _user_has_permission(self, user, **kwargs): + return user.is_default_user() -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def payment_remove_view(request: HttpRequest, id: str, payment_id: str): - """ Renders a modal view for removing payments - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - payment_id (str): The payment's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - payment = get_object_or_404(Payment, id=payment_id) - form = RemovePaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request) - return form.process_request( - request=request, - msg_success=PAYMENT_REMOVED, - redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data" - ) +class NewPaymentView(BasePaymentView): + _FORM_CLS = NewPaymentForm + _MSG_SUCCESS = PAYMENT_ADDED -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def payment_edit_view(request: HttpRequest, id: str, payment_id: str): - """ Renders a modal view for editing payments - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - payment_id (str): The payment's id - - Returns: - - """ - intervention = get_object_or_404(Intervention, id=id) - payment = get_object_or_404(Payment, id=payment_id) - form = EditPaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request) - return form.process_request( - request=request, - msg_success=PAYMENT_EDITED, - redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data" - ) +class EditPaymentView(BasePaymentView): + _MSG_SUCCESS = PAYMENT_EDITED + _FORM_CLS = EditPaymentModalForm +class RemovePaymentView(BasePaymentView): + _MSG_SUCCESS = PAYMENT_REMOVED + _FORM_CLS = RemovePaymentModalForm 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..8fc6c595 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, **kwargs): + 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, **kwargs): + 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, **kwargs): + return user.is_ets_user() diff --git a/ema/views/deadline.py b/ema/views/deadline.py index d760bdab..21ad0bf2 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, **kwargs): + 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, **kwargs): + 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, **kwargs): + return user.is_ets_user() diff --git a/ema/views/document.py b/ema/views/document.py index 45c58146..002304f0 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, **kwargs): + 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, **kwargs): + 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, **kwargs): + 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, **kwargs): + return user.is_ets_user() diff --git a/ema/views/ema.py b/ema/views/ema.py index 5322b5ef..7fcaa295 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -5,269 +5,113 @@ 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.identifier import AbstractIdentifierGeneratorView +from konova.views.form import AbstractNewGeometryFormView, AbstractEditGeometryFormView +from konova.views.index import AbstractIndexView +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, AbstractIndexView): + _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(AbstractNewGeometryFormView): + _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, **kwargs): + # 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(AbstractEditGeometryFormView): + _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, **kwargs): + # 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, AbstractIdentifierGeneratorView): + _MODEL_CLS = Ema + _REDIRECT_URL = "ema:index" + + def _user_has_permission(self, user, **kwargs): + 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, **kwargs): + return user.is_ets_user() diff --git a/ema/views/log.py b/ema/views/log.py index 82162ba4..3baf3886 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, **kwargs): + 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..fb499859 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, **kwargs): + return user.is_ets_user() diff --git a/ema/views/share.py b/ema/views/share.py index 00b75e7c..39fc109d 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, **kwargs): + return user.is_ets_user() \ No newline at end of file diff --git a/ema/views/state.py b/ema/views/state.py index e8e489dc..f28ca698 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, **kwargs): + 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, **kwargs): + 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, **kwargs): + 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/remove.py b/intervention/forms/modals/remove.py new file mode 100644 index 00000000..7052d8f3 --- /dev/null +++ b/intervention/forms/modals/remove.py @@ -0,0 +1,22 @@ +""" +Author: Michel Peltriaux +Created on: 08.11.25 + +""" +from django.shortcuts import get_object_or_404 + +from compensation.models import Compensation +from konova.forms.modals import RemoveModalForm + + +class RemoveCompensationFromInterventionModalForm(RemoveModalForm): + """ Specific form for removing a compensation from an intervention + + """ + def __init__(self, *args, **kwargs): + # The 'instance' that is pushed into the constructor by the generic base class points to the + # intervention instead of the compensation, which shall be deleted. Therefore we need to fetch the compensation + # and replace the instance parameter with that object + instance = get_object_or_404(Compensation, id=kwargs.pop("comp_id")) + kwargs["instance"] = instance + super().__init__(*args, **kwargs) 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..bf5f754a 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -8,39 +8,40 @@ 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.compensation import remove_compensation_view +from intervention.views.check import InterventionCheckView +from intervention.views.compensation import RemoveCompensationFromInterventionView 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 - path('/compensation//remove', remove_compensation_view, name='remove-compensation'), + path('/compensation//remove', RemoveCompensationFromInterventionView.as_view(), name='remove-compensation'), # Documents path('/document/new/', NewInterventionDocumentView.as_view(), name='new-doc'), @@ -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..927e126e 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.modal import AbstractModalFormView -@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, AbstractModalFormView): + _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, **kwargs): + 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/compensation.py b/intervention/views/compensation.py index 704a04e4..9e0ba095 100644 --- a/intervention/views/compensation.py +++ b/intervention/views/compensation.py @@ -5,42 +5,36 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.core.exceptions import ObjectDoesNotExist -from django.http import HttpRequest, Http404 +from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import get_object_or_404 from django.urls import reverse +from compensation.models import Compensation +from intervention.forms.modals.remove import RemoveCompensationFromInterventionModalForm from intervention.models import Intervention -from konova.decorators import shared_access_required, login_required_modal -from konova.forms.modals import RemoveModalForm from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE +from konova.views.remove import BaseRemoveModalFormView -@login_required_modal -@login_required -@shared_access_required(Intervention, "id") -def remove_compensation_view(request: HttpRequest, id: str, comp_id: str): - """ Renders a modal view for removing the compensation +class RemoveCompensationFromInterventionView(LoginRequiredMixin, BaseRemoveModalFormView): + _MODEL_CLS = Intervention + _FORM_CLS = RemoveCompensationFromInterventionModalForm + _MSG_SUCCESS = COMPENSATION_REMOVED_TEMPLATE + _REDIRECT_URL = "intervention:detail" - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id + def _user_has_shared_access(self, user, **kwargs): + compensation_id = kwargs.get("comp_id", None) + compensation = get_object_or_404(Compensation, id=compensation_id) + return compensation.is_shared_with(user) - Returns: + def _user_has_permission(self, user, **kwargs): + return user.is_default_user() - """ - intervention = get_object_or_404(Intervention, id=id) - try: - comp = intervention.compensations.get( - id=comp_id - ) - except ObjectDoesNotExist: - raise Http404("Unknown compensation") - form = RemoveModalForm(request.POST or None, instance=comp, request=request) - return form.process_request( - request=request, - msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier), - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data", - ) + def _get_msg_success(self, *args, **kwargs): + compensation_id = kwargs.get("comp_id", None) + compensation = get_object_or_404(Compensation, id=compensation_id) + return self._MSG_SUCCESS.format(compensation.identifier) + def _get_redirect_url(self, *args, **kwargs): + obj = kwargs.get("obj") + return reverse(self._REDIRECT_URL, args=(obj.id,)) + "#related_data" 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..6cbe7d13 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -5,293 +5,111 @@ 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.http import JsonResponse, HttpRequest -from django.shortcuts import get_object_or_404, render, redirect -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 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.forms import SimpleGeomForm -from konova.forms.modals import RemoveModalForm -from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ - CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \ - GEOMETRIES_IGNORED_TEMPLATE +from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE +from konova.views.identifier import AbstractIdentifierGeneratorView +from konova.views.form import AbstractNewGeometryFormView, AbstractEditGeometryFormView +from konova.views.index import AbstractIndexView +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, AbstractIndexView): + _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(AbstractNewGeometryFormView): + _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(AbstractEditGeometryFormView): + _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, AbstractIdentifierGeneratorView): + _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, } - ) + return context - -@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) - - -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing interventions - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "intervention/form/view.html" - # Get object from db - intervention = get_object_or_404(Intervention, id=id) - if intervention.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("intervention:detail", id=id) - - # Create forms, initialize with values from db/from POST request - data_form = EditInterventionForm(request.POST or None, instance=intervention) - geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention) - if request.method == "POST": - if data_form.is_valid() and geom_form.is_valid(): - # The data form takes the geom form for processing, as well as the performing user - # Save the current state of recorded|checked to inform the user in case of a status reset due to editing - intervention_is_checked = intervention.checked is not None - intervention = data_form.save(request.user, geom_form) - messages.success(request, _("Intervention {} edited").format(intervention.identifier)) - if intervention_is_checked: - messages.info(request, CHECK_STATE_RESET) - if geom_form.has_geometry_simplified(): - messages.info( - request, - GEOMETRY_SIMPLIFIED - ) - - num_ignored_geometries = geom_form.get_num_geometries_ignored() - if num_ignored_geometries > 0: - messages.info( - request, - GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) - ) - - return redirect("intervention:detail", id=intervention.id) - else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@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..8c17f0cf 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 AbstractReportView -@uuid_required -def report_view(request: HttpRequest, id: str): - """ Renders the public report view +class InterventionReportView(AbstractReportView): + _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..a67463bb 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.modal import AbstractModalFormView, AbstractBaseView -@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, AbstractModalFormView): + _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, **kwargs): + 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, AbstractBaseView): + _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, **kwargs): + 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 929e5216..8eb5f447 100644 --- a/konova/static/css/konova.css +++ b/konova/static/css/konova.css @@ -291,5 +291,5 @@ Overwrites netgis.css attributes } .netgis-menu{ - z-index: 1 !important; + 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..8e16fb41 100644 --- a/konova/utils/message_templates.py +++ b/konova/utils/message_templates.py @@ -19,7 +19,20 @@ 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") + +# USER | TEAM +TEAM_ADDED = _("New team added") +TEAM_EDITED = _("Team edited") +TEAM_REMOVED = _("Team removed") +TEAM_LEFT = _("Left Team") + +# 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 +107,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..8047543e 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.modal import AbstractModalFormView -class AbstractCompensationActionView(View): - model = None - redirect_url = None +class AbstractCompensationActionView(LoginRequiredMixin, AbstractModalFormView): + _MODEL_CLS = None + _REDIRECT_URL = None class Meta: abstract = True + def _user_has_permission(self, user, **kwargs): + 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..a8fc1378 --- /dev/null +++ b/konova/views/base.py @@ -0,0 +1,115 @@ +""" +Author: Michel Peltriaux +Created on: 15.10.25 + +""" +from abc import abstractmethod + +from django.contrib import messages +from django.http import HttpRequest +from django.shortcuts import redirect +from django.urls import reverse +from django.views import View +from konova.utils.general import check_user_is_in_any_group +from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED + + +class AbstractBaseView(View): + """ An abstract base view + + This class represents the root of all views on this project. It defines private variables which have to be used + by inheriting classes for proper generic inheriting. + + """ + _TEMPLATE: str = "CHANGE_ME" # Path to template file + _TAB_TITLE: str = "CHANGE_ME" # Title displayed on browser tab + _REDIRECT_URL: str = "CHANGE_ME" # Default URL to redirect after processing (notation as django url "namespace:endpoint") + _REDIRECT_URL_ERROR: str = "home" # Default URL to redirect in case of an error (same notation) + + class Meta: + abstract = True + + def dispatch(self, request, *args, **kwargs): + """ Dispatching requests before forwarding them into GET or POST endpoints. + + Defines basic checks which need to be done before a user can get access to any view inheriting from + this class. + + Args: + request (HttpRequest): The incoming request + *args (): + **kwargs (): + + Returns: + + """ + request = check_user_is_in_any_group(request) + + if not self._user_has_permission(request.user, **kwargs): + 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, **kwargs): + """ Checks whether the user has permission to get this view rendered. + + If no specific check is needed, this method can be overwritten with a simple True returning. + + Args: + user (User): The performing user + **kwargs (): + + Returns: + has_permission (bool): Whether the user has permission to see this view + """ + raise NotImplementedError("User permission not checked!") + + @abstractmethod + def _user_has_shared_access(self, user, **kwargs): + """ Checks whether the user has shared access to this object. + + If no shared-access-check is needed, this method can be overwritten with a simple True returning. + + Args: + user (User): The performing user + **kwargs (): + + Returns: + has_shared_access (bool): Whether the user has shared access + """ + raise NotImplementedError("Shared access not checked!") + + def _get_redirect_url(self, *args, **kwargs): + """ Getter to construct a more specific, data dependant redirect URL + + By default the method simply returns the pre-defined redirect URL. + + Args: + *args (): + **kwargs (): + + Returns: + url (str): Reversed redirect url + """ + return self._REDIRECT_URL + + def _get_redirect_url_error(self, *args, **kwargs): + """ Getter to construct a more specific, data dependant redirect URL in error cases + + By default the method simply returns the pre-defined redirect URL for errors. + + Args: + *args (): + **kwargs (): + + Returns: + url (str): Reversed redirect url + """ + return self._REDIRECT_URL_ERROR + diff --git a/konova/views/deadline.py b/konova/views/deadline.py index 4350073d..8b7c8cab 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.modal import AbstractModalFormView -class AbstractNewDeadlineView(View): - model = None - redirect_url = None +class AbstractNewDeadlineView(LoginRequiredMixin, AbstractModalFormView): + _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, **kwargs): + return user.is_default_user() -class AbstractEditDeadlineView(View): - model = None - redirect_url = None +class AbstractEditDeadlineView(LoginRequiredMixin, AbstractModalFormView): + _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, **kwargs): + return user.is_default_user() -class AbstractRemoveDeadlineView(View): - model = None - redirect_url = None +class AbstractRemoveDeadlineView(LoginRequiredMixin, AbstractModalFormView): + _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, **kwargs): + return user.is_default_user() diff --git a/konova/views/deduction.py b/konova/views/deduction.py index 5158f7b0..b549d084 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.modal import AbstractModalFormView -class AbstractDeductionView(View): - model = None - redirect_url = None +class AbstractDeductionView(AbstractModalFormView): + _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, **kwargs) -> 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..2c786f2d --- /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 AbstractBaseView + + +class BaseDetailView(LoginRequiredMixin, AbstractBaseView): + _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, **kwargs): + # 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..ea5d1b06 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.modal import AbstractModalFormView, AbstractBaseView -class AbstractNewDocumentView(View): - model = None - form = None - redirect_url = None +class AbstractNewDocumentView(LoginRequiredMixin, AbstractModalFormView): + _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, **kwargs): + return user.is_default_user() -class AbstractGetDocumentView(View): - model = None - document_model = None +class AbstractGetDocumentView(LoginRequiredMixin, AbstractBaseView): + _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, **kwargs): + 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, AbstractModalFormView): + _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, **kwargs): + 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, AbstractModalFormView): + _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, **kwargs): + 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/form.py b/konova/views/form.py new file mode 100644 index 00000000..0704b1ad --- /dev/null +++ b/konova/views/form.py @@ -0,0 +1,282 @@ +""" +Author: Michel Peltriaux +Created on: 12.12.25 + +""" +from django.contrib import messages +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 _ + +from konova.contexts import BaseContext +from konova.forms import BaseForm, SimpleGeomForm +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, \ + FORM_INVALID, IDENTIFIER_REPLACED +from konova.views.base import AbstractBaseView + + +class AbstractFormView(AbstractBaseView): + """ Abstract base class for rendering form views + + """ + _MODEL_CLS = None + _FORM_CLS = None + + class Meta: + abstract = True + + def _get_additional_context(self, **kwargs): + """ Getter for additional data, which is needed to properly render the current view + + Args: + **kwargs (): + + Returns: + context (dict): Additional context data for rendering + """ + return {} + + +class AbstractGeometryFormView(LoginRequiredMixin, AbstractFormView): + """ Abstract base view for processing objects with spatial data + + """ + _GEOMETRY_FORM_CLS = SimpleGeomForm + + class Meta: + abstract = True + + +class AbstractNewGeometryFormView(AbstractGeometryFormView): + """ Base view for creating new spatial data related to objects + + """ + + def _user_has_permission(self, user, **kwargs): + # 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, *args, **kwargs): + """ GET endpoint for rendering a form view where object data and spatial data are processed + + Args: + request (HttpRequest): The incoming request + **kwargs (): + + Returns: + + """ + # First initialize the regular object form and the geometry form based on request-bound data + form: BaseForm = self._FORM_CLS(None, **kwargs, user=request.user) + geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, user=request.user, read_only=False) + + # Get some additional context and put everything into the rendering pipeline + context = self._get_additional_context() + context = BaseContext(request, additional_context=context).context + context.update( + { + "form": form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + ) + return render(request, self._TEMPLATE, context) + + def post(self, request: HttpRequest, *args, **kwargs): + """ POST endpoint for processing object and spatial data provided by forms + + Args: + request (HttpRequest): The incoming request + **kwargs (): + + Returns: + + """ + # First initialize the regular object form and the geometry form based on request-bound data + form: BaseForm = self._FORM_CLS(request.POST or None, **kwargs, user=request.user) + geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(request.POST or None, user=request.user, read_only=False) + + # Only continue if both forms are without errors + if form.is_valid() and geom_form.is_valid(): + obj = form.save(request.user, geom_form) + obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,)) + + generated_identifier = form.cleaned_data.get("identifier", None) + + # There is a rare chance that an identifier has been taken already between sending the form and processing + # the data. If the identifier can not be used anymore, we have to inform the user that another identifier + # had to be generated + if generated_identifier != obj.identifier: + messages.info( + request, + IDENTIFIER_REPLACED.format( + generated_identifier, + obj.identifier + ) + ) + messages.success(request, _("{} added").format(obj.identifier)) + # Very complex geometries have to be simplified automatically while processing the spatial data. If this + # is the case, the user has to be informed. (They might want to check whether the stored geometry still + # fits their needs) + if geom_form.has_geometry_simplified(): + messages.info( + request, + GEOMETRY_SIMPLIFIED + ) + + # If certain parts of the geometry do not pass the quality check (e.g. way too small and therefore more like + # cutting errors) we need to inform the user that some parts have been removed/ignored while storing the + # geometry + num_ignored_geometries = geom_form.get_num_geometries_ignored() + if num_ignored_geometries > 0: + messages.info( + request, + GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) + ) + + return redirect(obj_redirect_url) + else: + # Something was not properly entered on the forms, so we have to inform the user + context = self._get_additional_context() + messages.error(request, FORM_INVALID, extra_tags="danger",) + + 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 AbstractEditGeometryFormView(AbstractGeometryFormView): + """ Base view for editing new spatial data related to objects + + """ + _TAB_TITLE = _("Edit {}") + + def get(self, request: HttpRequest, id: str, *args, **kwargs): + """ GET endpoint for rendering a form view where object data and spatial data are processed + + Args: + request (HttpRequest): The incoming request + id (str): The id of the object (not the geometry) + + Returns: + + """ + # First fetch the object identified by the id + obj = get_object_or_404( + self._MODEL_CLS, + id=id + ) + obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,)) + + # Check whether the object is recorded. If so - we can redirect the user and inform about the un-editability + # of this entry + if obj.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect(obj_redirect_url) + + # Seems like the object is not recorded. Good - initialize the forms based on the obj and request-bound data + form: BaseForm = self._FORM_CLS(None, instance=obj, user=request.user) + geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, instance=obj, read_only=False) + + # Get additional context for rendering and put everything in the rendering pipeline + context = self._get_additional_context() + context = BaseContext(request, additional_context=context).context + context.update( + { + "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, *args, **kwargs): + """ POST endpoint for processing object and spatial data provided by forms + + Args: + request (HttpRequest): The incoming request + id (str): The object's id + *args (): + **kwargs (): + + Returns: + + """ + obj = get_object_or_404( + self._MODEL_CLS, + id=id + ) + obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,)) + + # If the object is recorded, we abort the processing directly and inform the user + if obj.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect(obj_redirect_url) + + # Initialize forms with obj and request-bound data + form: BaseForm = self._FORM_CLS(request.POST or None, instance=obj, user=request.user) + geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(request.POST or None, instance=obj, read_only=False) + + if form.is_valid() and geom_form.is_valid(): + obj = form.save(request.user, geom_form) + messages.success(request, _("{} edited").format(obj.identifier)) + + # Very complex geometries have to be simplified automatically while processing the spatial data. If this + # is the case, the user has to be informed. (They might want to check whether the stored geometry still + # fits their needs) + if geom_form.has_geometry_simplified(): + messages.info( + request, + GEOMETRY_SIMPLIFIED + ) + + # If certain parts of the geometry do not pass the quality check (e.g. way too small and therefore more like + # cutting errors) we need to inform the user that some parts have been removed/ignored while storing the + # geometry + num_ignored_geometries = geom_form.get_num_geometries_ignored() + if num_ignored_geometries > 0: + messages.info( + 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, **kwargs): + return user.is_default_user() diff --git a/konova/views/geometry.py b/konova/views/geometry.py index 30aa8cd9..181400b2 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 AbstractBaseView -class GeomParcelsView(View): +class GeomParcelsView(AbstractBaseView): + _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, **kwargs): + return True + + +class GeomParcelsContentView(AbstractBaseView): + _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, **kwargs): + return True diff --git a/konova/views/home.py b/konova/views/home.py index 5253bbfe..796c36c6 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 AbstractBaseView from news.models import ServerMessage -class HomeView(LoginRequiredMixin, View): +class HomeView(LoginRequiredMixin, AbstractBaseView): + _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, **kwargs): + # 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/identifier.py b/konova/views/identifier.py new file mode 100644 index 00000000..c5a43aea --- /dev/null +++ b/konova/views/identifier.py @@ -0,0 +1,55 @@ +""" +Author: Michel Peltriaux +Created on: 12.12.25 + +""" +from django.http import HttpRequest, JsonResponse + +from konova.views.base import AbstractBaseView + + +class AbstractIdentifierGeneratorView(AbstractBaseView): + """ View class + + Process a request for generating a new identifier + + """ + _MODEL_CLS = None + _REDIRECT_URL: str = "home" + + class Meta: + abstract = True + + def get(self, request: HttpRequest): + """ GET endpoint + + Args: + request (): + + Returns: + + """ + 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, **kwargs): + """ 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 diff --git a/konova/views/index.py b/konova/views/index.py new file mode 100644 index 00000000..63165ed5 --- /dev/null +++ b/konova/views/index.py @@ -0,0 +1,65 @@ +""" +Author: Michel Peltriaux +Created on: 12.12.25 + +""" +from abc import abstractmethod + +from django.http import HttpRequest +from django.shortcuts import render + +from konova.contexts import BaseContext +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.views.base import AbstractBaseView + + +class AbstractIndexView(AbstractBaseView): + """ Abstract base class for all index views + + """ + _TEMPLATE: str = "generic_index.html" + _INDEX_TABLE_CLS = None + _REDIRECT_URL: str = "home" + + class Meta: + abstract = True + + def get(self, request: HttpRequest, *args, **kwargs): + """ GET endpoint for rendering index views + + Args: + request (HttpRequest): The incoming request + *args (): + **kwargs (): + + Returns: + + """ + qs = self._get_queryset() + table = self._INDEX_TABLE_CLS( + request=request, + 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): + """ Generic getter for the queryset of objects which shall be processed on this view + + Returns: + + """ + raise NotImplementedError + + def _user_has_permission(self, user, **kwargs): + # 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 diff --git a/konova/views/log.py b/konova/views/log.py index 7cc9d56a..0bbb2fb7 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 AbstractBaseView -class AbstractLogView(View): - model = None +class AbstractLogView(AbstractBaseView): + _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, **kwargs): + 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/modal.py b/konova/views/modal.py new file mode 100644 index 00000000..5269348e --- /dev/null +++ b/konova/views/modal.py @@ -0,0 +1,183 @@ +""" +Author: Michel Peltriaux +Created on: 12.12.25 + +""" +from bootstrap_modal_forms.mixins import is_ajax +from django.contrib import messages +from django.core.exceptions import ObjectDoesNotExist +from django.http import HttpRequest, HttpResponseRedirect +from django.shortcuts import get_object_or_404, render +from django.urls import reverse + +from konova.contexts import BaseContext +from konova.models import BaseObject +from konova.views.base import AbstractBaseView + + +class AbstractModalFormView(AbstractBaseView): + """ Abstract base view providing logic to perform most modal form based view renderings + + """ + _TEMPLATE: str = "modal/modal_form.html" + _MODEL_CLS = None + _FORM_CLS = None + _MSG_SUCCESS = None + + class Meta: + abstract = True + + def _user_has_shared_access(self, user, **kwargs): + """ Checks whether the user has shared access to this object. + + For objects inheriting from BaseObject class the method 'is_shared_with()' is a handy + wrapper for checking shared access. For any other circumstances this method should be overwritten + to provide custom shared-access-checking logic. + + If no shared-access-check is needed, this method can be overwritten with a simple True returning. + + Args: + user (User): The performing user + **kwargs (): + + Returns: + has_shared_access (bool): Whether the user has shared access + """ + obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get("id")) + return obj.is_shared_with(user) + + def get(self, request: HttpRequest, *args, **kwargs): + """ GET endpoint for rendering a view holding a modal form + + Args: + request (HttpRequest): The incoming request + *args (): + **kwargs (): + + Returns: + + """ + # If there is an id provided as mapped parameter from the URL -> take it ... + _id = kwargs.pop("id", None) + try: + # ... and try to resolve it into an object + obj = self._MODEL_CLS.objects.get(id=_id) + self._check_for_recorded_instance(obj) + except ObjectDoesNotExist: + # ... If there is none, maybe we are currently processing + # the creation of a new object (therefore no id yet), so let's continue + obj = None + form = self._FORM_CLS( + request.POST or None, + request.FILES or None, + instance=obj, + request=request, + **kwargs + ) + context = { + "form": form, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + def post(self, request: HttpRequest, *args, **kwargs): + """ POST endpoint for processing form contents of a view + + Args: + request (HttpRequest): The incoming request + *args (): + **kwargs (): + + Returns: + + """ + # If there is an id provided as mapped parameter from the URL take it ... + _id = kwargs.pop("id", None) + try: + # ... and try to resolve it into a record + obj = self._MODEL_CLS.objects.get(id=_id) + self._check_for_recorded_instance(obj) + except ObjectDoesNotExist: + # ... If there is none, maybe we are currently processing + # the creation of a new object (therefore no id yet), so let's continue + obj = None + form = self._FORM_CLS( + request.POST or None, + request.FILES or None, + instance=obj, + request=request, + **kwargs + ) + # Get now the redirect url and take specifics of the obj into account for that. + # We do not do this after saving the form to avoid side effects due to possibly changed data + redirect_url = self._get_redirect_url(obj=obj) + if form.is_valid(): + # Modal forms send one POST for checking on data validity. This is used to evaluate possible errors + # on the form. The second POST (if no errors have been found) is the 'proper' one, + # which we want to process by saving/commiting of the data to the database. + if not is_ajax(request.META): + # Get now the success message and take specifics of the obj into account for that + msg_success = self._get_msg_success(obj=obj, *args, **kwargs) + form.save() + messages.success( + 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): + """ Getter to construct a more specific, data dependant redirect URL (if needed) + + Args: + *args (): + **kwargs (): + + Returns: + url (str): Reversed redirect url + """ + obj = kwargs.get("obj", None) + if obj: + return reverse(self._REDIRECT_URL, args=(obj.id,)) + else: + return reverse(self._REDIRECT_URL) + + def _get_msg_success(self, *args, **kwargs): + """ Getter to construct a more specific, data dependant success message + + Args: + *args (): + **kwargs (): + + Returns: + + """ + return self._MSG_SUCCESS + + def _check_for_recorded_instance(self, obj): + """ Checks if the object on this view is recorded and runs some special logic if so + + If the instance is recorded, the view should provide some information about why the user can not edit anything. + This behaviour is only intended to mask any form for instances based on the BaseObject class. + + There are situations where the form should be rendered regularly, despite the instance being recorded, + e.g. for rendering deduction form contents on (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: + # Replace default template with a blocking one + self._TEMPLATE = "form/recorded_no_edit.html" diff --git a/konova/views/record.py b/konova/views/record.py index b80d1b0f..40d41adc 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.modal import AbstractModalFormView -class AbstractRecordView(View): - model = None +class AbstractRecordView(AbstractModalFormView): + _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, **kwargs): + 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..27ee1410 --- /dev/null +++ b/konova/views/remove.py @@ -0,0 +1,27 @@ +""" +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.modal import AbstractModalFormView + + +class BaseRemoveModalFormView(AbstractModalFormView): + _FORM_CLS = RemoveModalForm + _MSG_SUCCESS = GENERIC_REMOVED_TEMPLATE + _REDIRECT_URL = None + + def _user_has_permission(self, user, **kwargs): + 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..e6f0d7b1 --- /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 AbstractBaseView + + +class AbstractReportView(AbstractBaseView): + _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, **kwargs): + # 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..57698cd2 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.modal import AbstractModalFormView -class AbstractResubmissionView(View): - model = None - form_action_url_base = None - redirect_url_base = None +class AbstractResubmissionView(LoginRequiredMixin, AbstractModalFormView): + _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, **kwargs): + 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..f8680eab 100644 --- a/konova/views/share.py +++ b/konova/views/share.py @@ -6,24 +6,25 @@ 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 AbstractBaseView +from konova.views.modal import AbstractModalFormView -class AbstractShareByTokenView(View): - model = None - redirect_url = None +class AbstractShareByTokenView(LoginRequiredMixin, AbstractBaseView): + _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 +37,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 +52,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 +61,22 @@ class AbstractShareByTokenView(View): ) return redirect("home") + def _user_has_permission(self, user, **kwargs): + # 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, AbstractModalFormView): + _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, **kwargs): + return user.is_default_user() diff --git a/konova/views/state.py b/konova/views/state.py index 1165e08b..55da9319 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.modal import AbstractModalFormView -class AbstractCompensationStateView(View): - model = None - redirect_url = None +class AbstractCompensationStateView(LoginRequiredMixin, AbstractModalFormView): + _MODEL_CLS = None + _FORM_CLS = None + _REDIRECT_URL = None class Meta: abstract = True + def _user_has_permission(self, user, **kwargs): + 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..246917db 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..bc74bf27 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -4,16 +4,16 @@ # FIRST AUTHOR , YEAR. # #: compensation/filters/eco_account.py:21 -#: compensation/forms/modals/compensation_action.py:82 -#: compensation/forms/modals/deadline.py:52 -#: compensation/forms/modals/payment.py:24 -#: compensation/forms/modals/payment.py:35 -#: compensation/forms/modals/payment.py:52 +#: compensation/forms/modals/compensation_action.py:84 +#: compensation/forms/modals/deadline.py:53 +#: compensation/forms/modals/payment.py:26 +#: compensation/forms/modals/payment.py:37 +#: compensation/forms/modals/payment.py:54 #: intervention/forms/intervention.py:57 intervention/forms/intervention.py:177 #: intervention/forms/intervention.py:190 -#: intervention/forms/modals/revocation.py:21 -#: intervention/forms/modals/revocation.py:35 -#: intervention/forms/modals/revocation.py:48 +#: intervention/forms/modals/revocation.py:22 +#: intervention/forms/modals/revocation.py:36 +#: intervention/forms/modals/revocation.py:49 #: konova/filters/mixins/file_number.py:17 #: konova/filters/mixins/file_number.py:18 #: konova/filters/mixins/geo_reference.py:25 @@ -35,7 +35,7 @@ #: konova/forms/modals/document_form.py:50 #: konova/forms/modals/document_form.py:62 #: konova/forms/modals/document_form.py:80 -#: konova/forms/modals/remove_form.py:23 +#: konova/forms/modals/remove_form.py:24 #: konova/forms/modals/resubmission_form.py:22 #: konova/forms/modals/resubmission_form.py:38 konova/forms/remove_form.py:25 #: konova/tests/unit/test_forms.py:59 user/forms/modals/api_token.py:17 @@ -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-11-08 13:03+0100\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \n" @@ -127,7 +127,7 @@ msgstr "" #: analysis/templates/analysis/reports/includes/eco_account/amount.html:3 #: analysis/templates/analysis/reports/includes/intervention/amount.html:3 #: analysis/templates/analysis/reports/includes/old_data/amount.html:3 -#: compensation/forms/modals/compensation_action.py:66 +#: compensation/forms/modals/compensation_action.py:68 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:34 #: intervention/templates/intervention/detail/includes/deductions.html:31 msgid "Amount" @@ -261,7 +261,7 @@ msgstr "" #: analysis/templates/analysis/reports/includes/eco_account/deductions.html:14 #: analysis/templates/analysis/reports/includes/eco_account/deductions.html:16 -#: compensation/forms/modals/state.py:59 +#: compensation/forms/modals/state.py:55 #: compensation/templates/compensation/detail/compensation/includes/states-after.html:36 #: compensation/templates/compensation/detail/compensation/includes/states-before.html:36 #: compensation/templates/compensation/detail/eco_account/includes/states-after.html:36 @@ -296,7 +296,7 @@ msgid "Compensation" msgstr "Kompensation" #: analysis/templates/analysis/reports/includes/intervention/compensated_by.html:21 -#: compensation/forms/modals/payment.py:65 +#: compensation/forms/modals/payment.py:67 msgid "Payment" msgstr "Zahlung" @@ -401,15 +401,15 @@ msgstr "Bezeichnung" msgid "An explanatory name" msgstr "Aussagekräftiger Titel" -#: compensation/forms/compensation.py:49 ema/forms.py:51 ema/forms.py:114 +#: compensation/forms/compensation.py:49 ema/forms.py:52 ema/forms.py:115 #: ema/tests/unit/test_forms.py:31 ema/tests/unit/test_forms.py:85 msgid "Compensation XY; Location ABC" msgstr "Kompensation XY; Flur ABC" #: compensation/forms/compensation.py:56 -#: compensation/forms/modals/compensation_action.py:81 -#: compensation/forms/modals/deadline.py:51 -#: compensation/forms/modals/payment.py:51 +#: compensation/forms/modals/compensation_action.py:83 +#: compensation/forms/modals/deadline.py:52 +#: compensation/forms/modals/payment.py:53 #: compensation/templates/compensation/detail/compensation/includes/actions.html:35 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:39 #: compensation/templates/compensation/detail/compensation/includes/documents.html:34 @@ -420,7 +420,7 @@ msgstr "Kompensation XY; Flur ABC" #: ema/templates/ema/detail/includes/deadlines.html:39 #: ema/templates/ema/detail/includes/documents.html:34 #: intervention/forms/intervention.py:203 -#: intervention/forms/modals/revocation.py:47 +#: intervention/forms/modals/revocation.py:48 #: intervention/templates/intervention/detail/includes/documents.html:39 #: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/revocation.html:38 @@ -431,7 +431,7 @@ msgid "Comment" msgstr "Kommentar" #: compensation/forms/compensation.py:58 -#: compensation/forms/modals/compensation_action.py:83 +#: compensation/forms/modals/compensation_action.py:85 #: intervention/forms/intervention.py:205 #: konova/forms/modals/resubmission_form.py:39 msgid "Additional comment" @@ -448,11 +448,18 @@ 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 msgid "New compensation" msgstr "Neue Kompensation" -#: compensation/forms/compensation.py:190 +#: compensation/forms/compensation.py:178 +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:201 msgid "Edit compensation" msgstr "Bearbeite Kompensation" @@ -475,7 +482,8 @@ 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:49 +#: compensation/views/eco_account/eco_account.py:118 msgid "New Eco-Account" msgstr "Neues Ökokonto" @@ -572,11 +580,11 @@ msgid "" "production?" msgstr "Optional: Handelt es sich um eine produktionsintegrierte Kompensation?" -#: compensation/forms/modals/compensation_action.py:29 +#: compensation/forms/modals/compensation_action.py:31 msgid "Action Type" msgstr "Maßnahmentyp" -#: compensation/forms/modals/compensation_action.py:32 +#: compensation/forms/modals/compensation_action.py:34 msgid "" "An action can consist of multiple different action types. All the selected " "action types are expected to be performed according to the amount and unit " @@ -586,162 +594,158 @@ msgstr "" "hier gewählten Einträge sollen mit der weiter unten angegebenen Einheit und " "Menge umgesetzt werden. " -#: compensation/forms/modals/compensation_action.py:37 -#: compensation/forms/modals/compensation_action.py:49 +#: compensation/forms/modals/compensation_action.py:39 +#: compensation/forms/modals/compensation_action.py:51 msgid "Action Type detail" msgstr "Zusatzmerkmal" -#: compensation/forms/modals/compensation_action.py:40 +#: compensation/forms/modals/compensation_action.py:42 msgid "Select the action type detail" msgstr "Zusatzmerkmal wählen" -#: compensation/forms/modals/compensation_action.py:54 +#: compensation/forms/modals/compensation_action.py:56 msgid "Unit" msgstr "Einheit" -#: compensation/forms/modals/compensation_action.py:57 +#: compensation/forms/modals/compensation_action.py:59 msgid "Select the unit" msgstr "Einheit wählen" -#: compensation/forms/modals/compensation_action.py:69 +#: compensation/forms/modals/compensation_action.py:71 msgid "Insert the amount" msgstr "Menge eingeben" -#: compensation/forms/modals/compensation_action.py:94 +#: compensation/forms/modals/compensation_action.py:96 #: compensation/tests/compensation/unit/test_forms.py:42 msgid "New action" msgstr "Neue Maßnahme" -#: compensation/forms/modals/compensation_action.py:95 +#: compensation/forms/modals/compensation_action.py:97 #: compensation/tests/compensation/unit/test_forms.py:43 msgid "Insert data for the new action" msgstr "Geben Sie die Daten der neuen Maßnahme ein" -#: compensation/forms/modals/compensation_action.py:119 +#: compensation/forms/modals/compensation_action.py:122 #: compensation/templates/compensation/detail/compensation/includes/actions.html:68 #: compensation/templates/compensation/detail/eco_account/includes/actions.html:67 -#: compensation/tests/compensation/unit/test_forms.py:84 +#: compensation/tests/compensation/unit/test_forms.py:88 #: ema/templates/ema/detail/includes/actions.html:65 msgid "Edit action" msgstr "Maßnahme bearbeiten" -#: compensation/forms/modals/deadline.py:22 +#: compensation/forms/modals/deadline.py:23 msgid "Deadline Type" msgstr "Fristart" -#: compensation/forms/modals/deadline.py:25 +#: compensation/forms/modals/deadline.py:26 msgid "Select the deadline type" msgstr "Fristart wählen" -#: compensation/forms/modals/deadline.py:34 +#: compensation/forms/modals/deadline.py:35 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:36 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:36 #: ema/templates/ema/detail/includes/deadlines.html:36 -#: intervention/forms/modals/revocation.py:20 +#: intervention/forms/modals/revocation.py:21 #: konova/forms/modals/resubmission_form.py:23 msgid "Date" msgstr "Datum" -#: compensation/forms/modals/deadline.py:37 +#: compensation/forms/modals/deadline.py:38 msgid "Select date" msgstr "Datum wählen" -#: compensation/forms/modals/deadline.py:53 -#: compensation/forms/modals/payment.py:53 -#: intervention/forms/modals/revocation.py:49 +#: compensation/forms/modals/deadline.py:54 +#: compensation/forms/modals/payment.py:55 +#: intervention/forms/modals/revocation.py:50 #: konova/forms/modals/document_form.py:63 msgid "Additional comment, maximum {} letters" msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" -#: compensation/forms/modals/deadline.py:65 +#: compensation/forms/modals/deadline.py:66 #: konova/tests/unit/test_deadline.py:29 msgid "New deadline" msgstr "Neue Frist" -#: compensation/forms/modals/deadline.py:66 +#: compensation/forms/modals/deadline.py:67 #: konova/tests/unit/test_deadline.py:30 msgid "Insert data for the new deadline" msgstr "Geben Sie die Daten der neuen Frist ein" -#: compensation/forms/modals/deadline.py:78 +#: compensation/forms/modals/deadline.py:79 #: konova/tests/unit/test_deadline.py:57 msgid "Please explain this 'other' type of deadline." msgstr "Bitte erklären Sie um welchen 'sonstigen' Termin es sich handelt." -#: compensation/forms/modals/deadline.py:95 +#: compensation/forms/modals/deadline.py:97 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:64 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:62 #: ema/templates/ema/detail/includes/deadlines.html:62 msgid "Edit deadline" msgstr "Frist/Termin bearbeiten" -#: compensation/forms/modals/payment.py:25 +#: compensation/forms/modals/payment.py:27 msgid "in Euro" msgstr "in Euro" -#: compensation/forms/modals/payment.py:34 +#: compensation/forms/modals/payment.py:36 #: intervention/templates/intervention/detail/includes/payments.html:31 msgid "Due on" msgstr "Fällig am" -#: compensation/forms/modals/payment.py:38 +#: compensation/forms/modals/payment.py:40 msgid "Due on which date" msgstr "Zahlung wird an diesem Datum erwartet" -#: compensation/forms/modals/payment.py:66 +#: compensation/forms/modals/payment.py:68 msgid "Add a payment for intervention '{}'" msgstr "Neue Ersatzzahlung zu Eingriff '{}' hinzufügen" -#: compensation/forms/modals/payment.py:89 +#: compensation/forms/modals/payment.py:91 msgid "If there is no date you can enter, please explain why." msgstr "Falls Sie kein Datum angeben können, erklären Sie bitte weshalb." -#: compensation/forms/modals/payment.py:108 +#: compensation/forms/modals/payment.py:111 #: intervention/templates/intervention/detail/includes/payments.html:67 msgid "Edit payment" msgstr "Zahlung bearbeiten" -#: compensation/forms/modals/state.py:33 +#: compensation/forms/modals/state.py:29 msgid "Biotope Type" msgstr "Biotoptyp" -#: compensation/forms/modals/state.py:36 +#: compensation/forms/modals/state.py:32 msgid "Select the biotope type" msgstr "Biotoptyp wählen" -#: compensation/forms/modals/state.py:40 compensation/forms/modals/state.py:52 +#: compensation/forms/modals/state.py:36 compensation/forms/modals/state.py:48 msgid "Biotope additional type" msgstr "Zusatzbezeichnung" -#: compensation/forms/modals/state.py:43 +#: compensation/forms/modals/state.py:39 msgid "Select an additional biotope type" msgstr "Zusatzbezeichnung wählen" -#: compensation/forms/modals/state.py:62 +#: compensation/forms/modals/state.py:58 #: intervention/forms/modals/deduction.py:49 msgid "in m²" msgstr "" -#: compensation/forms/modals/state.py:73 -#: compensation/tests/compensation/unit/test_forms.py:175 +#: compensation/forms/modals/state.py:71 +#: compensation/tests/compensation/unit/test_forms.py:179 msgid "New state" msgstr "Neuer Zustand" -#: compensation/forms/modals/state.py:74 -#: compensation/tests/compensation/unit/test_forms.py:176 +#: compensation/forms/modals/state.py:72 +#: compensation/tests/compensation/unit/test_forms.py:180 msgid "Insert data for the new state" msgstr "Geben Sie die Daten des neuen Zustandes ein" -#: compensation/forms/modals/state.py:91 konova/forms/modals/base_form.py:32 -msgid "Object removed" -msgstr "Objekt entfernt" - -#: compensation/forms/modals/state.py:146 +#: compensation/forms/modals/state.py:99 #: compensation/templates/compensation/detail/compensation/includes/states-after.html:62 #: compensation/templates/compensation/detail/compensation/includes/states-before.html:62 #: compensation/templates/compensation/detail/eco_account/includes/states-after.html:62 #: compensation/templates/compensation/detail/eco_account/includes/states-before.html:62 -#: compensation/tests/compensation/unit/test_forms.py:236 +#: compensation/tests/compensation/unit/test_forms.py:260 #: ema/templates/ema/detail/includes/states-after.html:60 #: ema/templates/ema/detail/includes/states-before.html:60 msgid "Edit state" @@ -937,7 +941,7 @@ msgstr "Öffentlicher Bericht" #: ema/templates/ema/detail/includes/controls.html:15 #: intervention/templates/intervention/detail/includes/controls.html:15 #: konova/forms/modals/resubmission_form.py:51 -#: konova/tests/unit/test_forms.py:302 konova/tests/unit/test_forms.py:316 +#: konova/tests/unit/test_forms.py:301 konova/tests/unit/test_forms.py:315 #: templates/email/resubmission/resubmission.html:4 msgid "Resubmission" msgstr "Wiedervorlage" @@ -1015,7 +1019,7 @@ msgstr "Erstellt" #: compensation/templates/compensation/detail/eco_account/includes/documents.html:61 #: ema/templates/ema/detail/includes/documents.html:61 #: intervention/templates/intervention/detail/includes/documents.html:70 -#: konova/forms/modals/document_form.py:141 konova/tests/unit/test_forms.py:118 +#: konova/forms/modals/document_form.py:143 konova/tests/unit/test_forms.py:118 msgid "Edit document" msgstr "Dokument bearbeiten" @@ -1228,7 +1232,7 @@ msgid "Recorded on" msgstr "Verzeichnet am" #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:65 -#: intervention/forms/modals/deduction.py:177 +#: intervention/forms/modals/deduction.py:178 #: intervention/templates/intervention/detail/includes/deductions.html:60 msgid "Edit Deduction" msgstr "Abbuchung bearbeiten" @@ -1288,48 +1292,37 @@ msgstr "" msgid "Responsible data" msgstr "Daten zu den verantwortlichen Stellen" -#: compensation/views/compensation/compensation.py:58 +#: compensation/views/compensation/compensation.py:32 msgid "Compensations - Overview" msgstr "Kompensationen - Übersicht" -#: compensation/views/compensation/compensation.py:181 -#: konova/utils/message_templates.py:40 -msgid "Compensation {} edited" -msgstr "Kompensation {} bearbeitet" +#: compensation/views/compensation/compensation.py:49 +msgid "New Compensation" +msgstr "Neue Kompensation" -#: compensation/views/compensation/compensation.py:196 -#: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:238 -#: intervention/views/intervention.py:253 -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:34 msgid "Eco-account - Overview" msgstr "Ökokonten - Übersicht" -#: compensation/views/eco_account/eco_account.py:86 +#: compensation/views/eco_account/eco_account.py:95 msgid "Eco-Account {} added" msgstr "Ökokonto {} hinzugefügt" -#: compensation/views/eco_account/eco_account.py:158 +#: compensation/views/eco_account/eco_account.py:161 msgid "Eco-Account {} edited" msgstr "Ökokonto {} bearbeitet" -#: compensation/views/eco_account/eco_account.py:288 -msgid "Eco-account removed" -msgstr "Ökokonto entfernt" +#: compensation/views/eco_account/eco_account.py:184 ema/views/ema.py:51 +#: intervention/views/intervention.py:58 intervention/views/intervention.py:178 +#: konova/views/base.py:524 +msgid "Edit {}" +msgstr "Bearbeite {}" -#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:108 +#: ema/forms.py:43 ema/tests/unit/test_forms.py:27 ema/views/ema.py:38 msgid "New EMA" msgstr "Neue EMA hinzufügen" -#: ema/forms.py:108 ema/tests/unit/test_forms.py:81 +#: ema/forms.py:109 ema/tests/unit/test_forms.py:81 msgid "Edit EMA" msgstr "Bearbeite EMA" @@ -1353,22 +1346,10 @@ msgstr "" msgid "Payment funded compensation" msgstr "Ersatzzahlungsmaßnahme" -#: ema/views/ema.py:53 +#: ema/views/ema.py:22 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 -msgid "EMA removed" -msgstr "EMA entfernt" - #: intervention/forms/intervention.py:49 msgid "Construction XY; Location ABC" msgstr "Bauvorhaben XY; Flur ABC" @@ -1429,7 +1410,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:50 msgid "New intervention" msgstr "Neuer Eingriff" @@ -1451,7 +1432,7 @@ msgid "Run check" msgstr "Prüfung vornehmen" #: intervention/forms/modals/check.py:36 konova/forms/modals/record_form.py:30 -#: konova/tests/unit/test_forms.py:155 +#: konova/tests/unit/test_forms.py:154 msgid "The necessary control steps have been performed:" msgstr "Die notwendigen Kontrollschritte wurden durchgeführt:" @@ -1495,26 +1476,26 @@ msgstr "" "Das Ökokonto {} hat für eine Abbuchung von {} m² nicht ausreichend " "Restfläche. Es stehen noch {} m² zur Verfügung." -#: intervention/forms/modals/revocation.py:22 +#: intervention/forms/modals/revocation.py:23 msgid "Date of revocation" msgstr "Datum des Widerspruchs" -#: intervention/forms/modals/revocation.py:34 +#: intervention/forms/modals/revocation.py:35 #: intervention/templates/intervention/detail/includes/revocation.html:35 msgid "Document" msgstr "Dokument" -#: intervention/forms/modals/revocation.py:37 +#: intervention/forms/modals/revocation.py:38 msgid "Must be smaller than 15 Mb" msgstr "Muss kleiner als 15 Mb sein" -#: intervention/forms/modals/revocation.py:62 +#: intervention/forms/modals/revocation.py:63 #: intervention/templates/intervention/detail/includes/revocation.html:18 #: intervention/tests/unit/test_forms.py:234 msgid "Add revocation" msgstr "Widerspruch hinzufügen" -#: intervention/forms/modals/revocation.py:80 +#: intervention/forms/modals/revocation.py:82 #: intervention/templates/intervention/detail/includes/revocation.html:69 msgid "Edit revocation" msgstr "Widerspruch bearbeiten" @@ -1599,7 +1580,7 @@ msgid "Amount" msgstr "Betrag" #: intervention/templates/intervention/detail/includes/payments.html:61 -#: konova/utils/message_templates.py:25 +#: konova/utils/message_templates.py:38 msgid "This data is not shared with you" msgstr "Diese Daten sind für Sie nicht freigegeben" @@ -1661,26 +1642,18 @@ msgstr "" "Kein Ausgleich jeglicher Art gefunden (Kompensation, Ersatzzahlung, " "Abbuchung)" -#: intervention/views/check.py:36 +#: intervention/views/check.py:19 msgid "Check performed" msgstr "Prüfung durchgeführt" -#: intervention/views/intervention.py:57 +#: intervention/views/intervention.py:32 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:153 msgid "Intervention {} edited" msgstr "Eingriff {} bearbeitet" -#: intervention/views/intervention.py:278 -msgid "{} removed" -msgstr "{} entfernt" - #: konova/decorators.py:32 msgid "You need to be staff to perform this action!" msgstr "Hierfür müssen Sie Mitarbeiter sein!" @@ -1689,7 +1662,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. +++" @@ -1797,11 +1770,11 @@ msgid "" "history" msgstr "Sucht nach Einträgen, an denen diese Person gearbeitet hat" -#: konova/forms/base_form.py:23 templates/form/collapsable/form.html:62 +#: konova/forms/base_form.py:19 templates/form/collapsable/form.html:62 msgid "Save" msgstr "Speichern" -#: konova/forms/base_form.py:72 +#: konova/forms/base_form.py:69 msgid "Not editable" msgstr "Nicht editierbar" @@ -1810,11 +1783,15 @@ 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." +#: konova/forms/modals/base_form.py:32 +msgid "Object removed" +msgstr "Objekt entfernt" + #: konova/forms/modals/document_form.py:37 msgid "When has this file been created? Important for photos." msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?" @@ -1835,35 +1812,35 @@ msgstr "Dokument hinzugefügt" msgid "Confirm record" msgstr "Verzeichnen bestätigen" -#: konova/forms/modals/record_form.py:29 konova/tests/unit/test_forms.py:153 +#: konova/forms/modals/record_form.py:29 konova/tests/unit/test_forms.py:152 msgid "Record data" msgstr "Daten verzeichnen" -#: konova/forms/modals/record_form.py:36 konova/tests/unit/test_forms.py:168 +#: konova/forms/modals/record_form.py:36 konova/tests/unit/test_forms.py:167 msgid "Confirm unrecord" msgstr "Entzeichnen bestätigen" -#: konova/forms/modals/record_form.py:37 konova/tests/unit/test_forms.py:167 +#: konova/forms/modals/record_form.py:37 konova/tests/unit/test_forms.py:166 msgid "Unrecord data" msgstr "Daten entzeichnen" -#: konova/forms/modals/record_form.py:38 konova/tests/unit/test_forms.py:170 +#: konova/forms/modals/record_form.py:38 konova/tests/unit/test_forms.py:169 msgid "I, {} {}, confirm that this data must be unrecorded." msgstr "" "Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen." -#: konova/forms/modals/remove_form.py:22 konova/forms/remove_form.py:24 +#: konova/forms/modals/remove_form.py:23 konova/forms/remove_form.py:24 #: user/forms/modals/api_token.py:16 msgid "Confirm" msgstr "Bestätige" -#: konova/forms/modals/remove_form.py:32 konova/forms/remove_form.py:36 -#: konova/tests/unit/test_forms.py:209 konova/tests/unit/test_forms.py:261 +#: konova/forms/modals/remove_form.py:33 konova/forms/remove_form.py:36 +#: konova/tests/unit/test_forms.py:208 konova/tests/unit/test_forms.py:260 msgid "Remove" msgstr "Löschen" -#: konova/forms/modals/remove_form.py:33 konova/tests/unit/test_forms.py:210 -#: konova/tests/unit/test_forms.py:262 +#: konova/forms/modals/remove_form.py:34 konova/tests/unit/test_forms.py:209 +#: konova/tests/unit/test_forms.py:261 msgid "Are you sure?" msgstr "Sind Sie sicher?" @@ -1872,7 +1849,7 @@ msgid "When do you want to be reminded?" msgstr "Wann wollen Sie erinnert werden?" #: konova/forms/modals/resubmission_form.py:52 -#: konova/tests/unit/test_forms.py:303 konova/tests/unit/test_forms.py:317 +#: konova/tests/unit/test_forms.py:302 konova/tests/unit/test_forms.py:316 msgid "Set your resubmission for this entry." msgstr "Setzen Sie eine Wiedervorlage für diesen Eintrag." @@ -2097,14 +2074,42 @@ msgstr "Hierfür müssen Sie einer anderen Nutzergruppe angehören!" msgid "Status of Checked reset" msgstr "Status 'Geprüft' wurde zurückgesetzt" -#: konova/utils/message_templates.py:22 +#: konova/utils/message_templates.py:24 +msgid "New team added" +msgstr "Neues Team hinzugefügt" + +#: konova/utils/message_templates.py:25 +msgid "Team edited" +msgstr "Team bearbeitet" + +#: konova/utils/message_templates.py:26 +msgid "Team removed" +msgstr "Team gelöscht" + +#: konova/utils/message_templates.py:27 +msgid "Left Team" +msgstr "Team verlassen" + +#: konova/utils/message_templates.py:30 +msgid "{} removed" +msgstr "{} entfernt" + +#: konova/utils/message_templates.py:33 msgid "" "Entry is recorded. To edit data, the entry first needs to be unrecorded." msgstr "" "Eintrag ist verzeichnet. Um Daten zu bearbeiten, muss der Eintrag erst " "entzeichnet werden." -#: konova/utils/message_templates.py:26 +#: konova/utils/message_templates.py:34 +msgid "{} recorded" +msgstr "{} verzeichnet" + +#: konova/utils/message_templates.py:35 +msgid "{} unrecorded" +msgstr "{} entzeichnet" + +#: konova/utils/message_templates.py:39 msgid "" "Remember: This data has not been shared with you, yet. This means you can " "only read but can not edit or perform any actions like running a check or " @@ -2114,11 +2119,11 @@ msgstr "" "bedeutet, dass Sie nur lesenden Zugriff hierauf haben und weder bearbeiten, " "noch Prüfungen durchführen oder verzeichnen können." -#: konova/utils/message_templates.py:27 +#: konova/utils/message_templates.py:40 msgid "Share settings updated" msgstr "Freigabe Einstellungen aktualisiert" -#: konova/utils/message_templates.py:28 +#: konova/utils/message_templates.py:41 msgid "" "Do not forget to share your entry! Currently you are the only one having " "shared access." @@ -2126,15 +2131,15 @@ msgstr "" "Denken Sie daran Ihren Eintrag freizugeben! Aktuell haben nur Sie eine " "Freigabe hierauf." -#: konova/utils/message_templates.py:31 +#: konova/utils/message_templates.py:44 msgid "Unsupported file type" msgstr "Dateiformat nicht unterstützt" -#: konova/utils/message_templates.py:32 +#: konova/utils/message_templates.py:45 msgid "File too large" msgstr "Datei zu groß" -#: konova/utils/message_templates.py:35 +#: konova/utils/message_templates.py:48 msgid "" "Action canceled. Eco account is recorded or deductions exist. Only " "conservation office member can perform this action." @@ -2142,119 +2147,123 @@ msgstr "" "Aktion abgebrochen. Ökokonto ist bereits verzeichnet oder Abbuchungen liegen " "vor. Nur Eintragungsstellennutzer können diese Aktion jetzt durchführen." -#: konova/utils/message_templates.py:38 +#: konova/utils/message_templates.py:51 msgid "Compensation {} added" msgstr "Kompensation {} hinzugefügt" -#: konova/utils/message_templates.py:39 +#: konova/utils/message_templates.py:52 msgid "Compensation {} removed" msgstr "Kompensation {} entfernt" -#: konova/utils/message_templates.py:41 +#: konova/utils/message_templates.py:53 +msgid "Compensation {} edited" +msgstr "Kompensation {} bearbeitet" + +#: konova/utils/message_templates.py:54 msgid "Added compensation action" msgstr "Maßnahme hinzugefügt" -#: konova/utils/message_templates.py:42 +#: konova/utils/message_templates.py:55 msgid "Added compensation state" msgstr "Zustand hinzugefügt" -#: konova/utils/message_templates.py:45 +#: konova/utils/message_templates.py:58 msgid "State removed" msgstr "Zustand gelöscht" -#: konova/utils/message_templates.py:46 +#: konova/utils/message_templates.py:59 msgid "State edited" msgstr "Zustand bearbeitet" -#: konova/utils/message_templates.py:47 +#: konova/utils/message_templates.py:60 msgid "State added" msgstr "Zustand hinzugefügt" -#: konova/utils/message_templates.py:50 +#: konova/utils/message_templates.py:63 msgid "Action added" msgstr "Maßnahme hinzugefügt" -#: konova/utils/message_templates.py:51 +#: konova/utils/message_templates.py:64 msgid "Action edited" msgstr "Maßnahme bearbeitet" -#: konova/utils/message_templates.py:52 +#: konova/utils/message_templates.py:65 msgid "Action removed" msgstr "Maßnahme entfernt" -#: konova/utils/message_templates.py:55 +#: konova/utils/message_templates.py:68 msgid "Deduction added" msgstr "Abbuchung hinzugefügt" -#: konova/utils/message_templates.py:56 +#: konova/utils/message_templates.py:69 msgid "Deduction edited" msgstr "Abbuchung bearbeitet" -#: konova/utils/message_templates.py:57 +#: konova/utils/message_templates.py:70 msgid "Deduction removed" msgstr "Abbuchung entfernt" -#: konova/utils/message_templates.py:58 +#: konova/utils/message_templates.py:71 msgid "Unknown deduction" msgstr "Unbekannte Abbuchung" -#: konova/utils/message_templates.py:61 +#: konova/utils/message_templates.py:74 msgid "Deadline added" msgstr "Frist/Termin hinzugefügt" -#: konova/utils/message_templates.py:62 +#: konova/utils/message_templates.py:75 msgid "Deadline edited" msgstr "Frist/Termin bearbeitet" -#: konova/utils/message_templates.py:63 +#: konova/utils/message_templates.py:76 msgid "Deadline removed" msgstr "Frist/Termin gelöscht" -#: konova/utils/message_templates.py:66 +#: konova/utils/message_templates.py:79 msgid "Payment added" msgstr "Zahlung hinzugefügt" -#: konova/utils/message_templates.py:67 +#: konova/utils/message_templates.py:80 msgid "Payment edited" msgstr "Zahlung bearbeitet" -#: konova/utils/message_templates.py:68 +#: konova/utils/message_templates.py:81 msgid "Payment removed" msgstr "Zahlung gelöscht" -#: konova/utils/message_templates.py:71 +#: konova/utils/message_templates.py:84 msgid "Revocation added" msgstr "Widerspruch hinzugefügt" -#: konova/utils/message_templates.py:72 +#: konova/utils/message_templates.py:85 msgid "Revocation edited" msgstr "Widerspruch bearbeitet" -#: konova/utils/message_templates.py:73 +#: konova/utils/message_templates.py:86 msgid "Revocation removed" msgstr "Widerspruch entfernt" -#: konova/utils/message_templates.py:76 +#: konova/utils/message_templates.py:89 msgid "Document '{}' deleted" msgstr "Dokument '{}' gelöscht" -#: konova/utils/message_templates.py:77 +#: konova/utils/message_templates.py:90 msgid "Document added" msgstr "Dokument hinzugefügt" -#: konova/utils/message_templates.py:78 +#: konova/utils/message_templates.py:91 msgid "Document edited" msgstr "Dokument bearbeitet" -#: konova/utils/message_templates.py:81 +#: konova/utils/message_templates.py:94 msgid "Edited general data" msgstr "Allgemeine Daten bearbeitet" -#: konova/utils/message_templates.py:84 +#: konova/utils/message_templates.py:97 msgid "Geometry conflict detected with {}" msgstr "Geometriekonflikt mit folgenden Einträgen erkannt: {}" -#: konova/utils/message_templates.py:85 +#: konova/utils/message_templates.py:98 msgid "" "The geometry contained more than {} vertices. It had to be simplified to " "match the allowed limit of {} vertices." @@ -2262,36 +2271,41 @@ msgstr "" "Die Geometrie enthielt mehr als {} Eckpunkte. Sie musste vereinfacht werden " "um die Obergrenze von {} erlaubten Eckpunkten einzuhalten." -#: konova/utils/message_templates.py:86 +#: konova/utils/message_templates.py:99 msgid "" "The geometry contained {} parts which have been detected as invalid (e.g. " "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 +#: konova/utils/message_templates.py:102 msgid "This intervention has {} revocations" msgstr "Dem Eingriff liegen {} Widersprüche vor" -#: konova/utils/message_templates.py:92 +#: konova/utils/message_templates.py:105 msgid "Checked on {} by {}" msgstr "Am {} von {} geprüft worden" -#: konova/utils/message_templates.py:93 +#: konova/utils/message_templates.py:106 msgid "Data has changed since last check on {} by {}" msgstr "" "Daten wurden nach der letzten Prüfung geändert. Letzte Prüfung am {} durch {}" -#: konova/utils/message_templates.py:94 +#: konova/utils/message_templates.py:107 msgid "Current data not checked yet" msgstr "Momentane Daten noch nicht geprüft" -#: konova/utils/message_templates.py:97 +#: konova/utils/message_templates.py:110 msgid "New token generated. Administrators need to validate." msgstr "Neuer Token generiert. Administratoren sind informiert." +#: konova/utils/message_templates.py:113 +msgid "Resubmission set" +msgstr "Wiedervorlage gesetzt" + #: konova/utils/quality.py:32 msgid "missing" msgstr "fehlend" @@ -2310,7 +2324,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:483 +msgid "{} added" +msgstr "{} hinzugefügt" + +#: konova/views/base.py:600 +msgid "{} edited" +msgstr "{} bearbeitet" + +#: konova/views/home.py:72 templates/navbars/navbar.html:16 msgid "Home" msgstr "Home" @@ -2318,21 +2340,9 @@ msgstr "Home" msgid "Log" msgstr "Log" -#: konova/views/record.py:30 -msgid "{} unrecorded" -msgstr "{} entzeichnet" - -#: konova/views/record.py:30 -msgid "{} recorded" -msgstr "{} verzeichnet" - -#: konova/views/record.py:35 -msgid "Errors found:" -msgstr "Fehler gefunden:" - -#: konova/views/resubmission.py:39 -msgid "Resubmission set" -msgstr "Wiedervorlage gesetzt" +#: konova/views/report.py:21 +msgid "Report {}" +msgstr "Bericht {}" #: konova/views/share.py:46 msgid "{} has already been shared with you" @@ -2874,11 +2884,11 @@ msgstr "" "Falls die Geometrie nicht leer ist, werden die Flurstücke aktuell berechnet. " "Bitte laden Sie diese Seite in ein paar Augenblicken erneut..." -#: user/forms/modals/api_token.py:25 +#: user/forms/modals/api_token.py:26 msgid "Generate API Token" msgstr "API Token generieren" -#: user/forms/modals/api_token.py:29 +#: user/forms/modals/api_token.py:30 msgid "" "You are about to create a new API token. The existing one will not be usable " "afterwards." @@ -2886,7 +2896,7 @@ msgstr "" "Wenn Sie fortfahren, generieren Sie einen neuen API Token. Ihren " "existierenden werden Sie dann nicht länger nutzen können." -#: user/forms/modals/api_token.py:31 +#: user/forms/modals/api_token.py:32 msgid "A new token needs to be validated by an administrator!" msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!" @@ -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/teams.py:36 msgid "Teams" msgstr "" @@ -3112,38 +3122,36 @@ msgstr "Token noch nicht freigeschaltet" msgid "Valid until" msgstr "Läuft ab am" -#: user/views/api_token.py:34 +#: user/views/api_token.py:36 msgid "User API token" msgstr "API Nutzer Token" -#: user/views/views.py:33 +#: user/views/users.py:26 msgid "User settings" msgstr "Einstellungen" -#: user/views/views.py:59 -msgid "Notifications edited" -msgstr "Benachrichtigungen bearbeitet" - -#: user/views/views.py:71 +#: user/views/users.py:39 msgid "User notifications" msgstr "Benachrichtigungen" -#: user/views/views.py:147 -msgid "New team added" -msgstr "Neues Team hinzugefügt" +#: user/views/users.py:59 +msgid "Notifications edited" +msgstr "Benachrichtigungen bearbeitet" -#: user/views/views.py:162 -msgid "Team edited" -msgstr "Team bearbeitet" +#~ msgid "Eco-account removed" +#~ msgstr "Ökokonto entfernt" -#: user/views/views.py:177 -msgid "Team removed" -msgstr "Team gelöscht" +#~ msgid "EMA removed" +#~ msgstr "EMA entfernt" -#: user/views/views.py:192 -msgid "You are not a member of this team" -msgstr "Sie sind kein Mitglied dieses Teams" +#~ msgid "Errors found:" +#~ msgstr "Fehler gefunden:" -#: user/views/views.py:199 -msgid "Left Team" -msgstr "Team verlassen" +#~ msgid "You are not a member of this team" +#~ msgstr "Sie sind kein Mitglied dieses Teams" + +#~ 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/modals/api_token.py b/user/forms/modals/api_token.py index d5ead620..5db9f3fb 100644 --- a/user/forms/modals/api_token.py +++ b/user/forms/modals/api_token.py @@ -22,6 +22,7 @@ class NewAPITokenModalForm(BaseModalForm): def __init__(self, *args, **kwargs): self.template = "modal/modal_form.html" super().__init__(*args, **kwargs) + self.instance = self.user self.form_title = _("Generate API Token") self.form_caption = "" diff --git a/user/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..e036743c 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() @@ -260,7 +260,7 @@ class ApiTokenFormTestCase(BaseTestCase): } self.assertIsNone(self.user.api_token) - form = NewAPITokenModalForm(request.POST, instance=self.user) + form = NewAPITokenModalForm(request.POST, request=request) form.save() self.user.refresh_from_db() token = self.user.api_token diff --git a/user/urls.py b/user/urls.py index c3127a1e..14ca7ce5 100644 --- a/user/urls.py +++ b/user/urls.py @@ -9,24 +9,26 @@ from django.urls import path from user.autocomplete.share import ShareUserAutocomplete, ShareTeamAutocomplete from user.autocomplete.team import TeamAdminAutocomplete -from user.views.api_token import APITokenView, new_api_token_view +from user.views.api_token import APITokenView, NewAPITokenView from user.views.propagate import PropagateUserView -from user.views.views import * +from user.views.teams import TeamIndexView, NewTeamView, TeamDetailModalView, EditTeamView, RemoveTeamView, \ + LeaveTeamView +from user.views.users import UserDetailView, NotificationsView, ContactView app_name = "user" urlpatterns = [ - 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("team/new", new_team_view, name="team-new"), - path("team/", data_team_view, name="team-data"), - path("team//edit", edit_team_view, name="team-edit"), - path("team//remove", remove_team_view, name="team-remove"), - path("team//leave", leave_team_view, name="team-leave"), + path("token/api/new", NewAPITokenView.as_view(), name="api-token-new"), + path("contact/", ContactView.as_view(), name="contact"), + path("team/", TeamIndexView.as_view(), name="team-index"), + path("team/new", NewTeamView.as_view(), name="team-new"), + path("team/", TeamDetailModalView.as_view(), name="team-data"), + path("team//edit", EditTeamView.as_view(), name="team-edit"), + path("team//remove", RemoveTeamView.as_view(), name="team-remove"), + path("team//leave", LeaveTeamView.as_view(), name="team-leave"), # Autocomplete urls path("atcmplt/share/u", ShareUserAutocomplete.as_view(), name="share-user-autocomplete"), diff --git a/user/views/api_token.py b/user/views/api_token.py index 3b653c28..125b9e78 100644 --- a/user/views/api_token.py +++ b/user/views/api_token.py @@ -4,9 +4,9 @@ Created on: 08.01.25 """ from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.http import HttpRequest from django.shortcuts import render -from django.urls import reverse from django.utils.decorators import method_decorator from django.views import View from django.utils.translation import gettext_lazy as _ @@ -15,7 +15,9 @@ from konova.contexts import BaseContext from konova.decorators import default_group_required from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import NEW_API_TOKEN_GENERATED +from konova.views.modal import AbstractModalFormView from user.forms.modals.api_token import NewAPITokenModalForm +from user.models import User class APITokenView(View): @@ -36,22 +38,16 @@ class APITokenView(View): context = BaseContext(request, context).context return render(request, template, context) +class NewAPITokenView(LoginRequiredMixin, AbstractModalFormView): + _MODEL_CLS = User + _FORM_CLS = NewAPITokenModalForm + _MSG_SUCCESS = NEW_API_TOKEN_GENERATED + _REDIRECT_URL = "user:api-token" -def new_api_token_view(request: HttpRequest): - """ Function based view for processing ModalForm - (Currently ModalForms only work properly with function based views) + def _user_has_shared_access(self, user, **kwargs): + # No special checks to be done in here + return True - Args: - request (): - - Returns: - - """ - user = request.user - - form = NewAPITokenModalForm(request.POST or None, instance=user, request=request) - return form.process_request( - request=request, - msg_success=NEW_API_TOKEN_GENERATED, - redirect_url=reverse("user:api-token"), - ) \ No newline at end of file + def _user_has_permission(self, user, **kwargs): + # User should at least be a default user to be able to use the api + return user.is_default_user() diff --git a/user/views/teams.py b/user/views/teams.py new file mode 100644 index 00000000..d9be7fe2 --- /dev/null +++ b/user/views/teams.py @@ -0,0 +1,105 @@ +""" +Author: Michel Peltriaux +Created on: 05.11.25 + +""" +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import Http404, HttpRequest +from django.shortcuts import get_object_or_404, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from konova.contexts import BaseContext +from konova.utils.message_templates import TEAM_LEFT, TEAM_REMOVED, TEAM_EDITED, TEAM_ADDED +from konova.views.modal import AbstractModalFormView +from user.forms.modals.team import LeaveTeamModalForm, RemoveTeamModalForm, EditTeamModalForm, NewTeamModalForm +from user.forms.team import TeamDataForm +from user.models import Team +from user.views.users import UserBaseView + + +class TeamDetailModalView(LoginRequiredMixin, AbstractModalFormView): + _FORM_CLS = TeamDataForm + _MODEL_CLS = Team + + def _user_has_shared_access(self, user, **kwargs): + # No specific constraints + return True + + def _user_has_permission(self, user, **kwargs): + # No specific constraints + return True + + +class TeamIndexView(LoginRequiredMixin, UserBaseView): + _TEMPLATE = "user/team/index.html" + _TAB_TITLE = _("Teams") + + def get(self, request: HttpRequest): + user = request.user + context = { + "teams": user.shared_teams, + "tab_title": self._TAB_TITLE, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + +class BaseTeamView(LoginRequiredMixin, AbstractModalFormView): + _REDIRECT_URL = "user:team-index" + _MODEL_CLS = Team + + class Meta: + abstract = True + + def _user_has_permission(self, user, **kwargs): + # Nothing to check here - just pass the test + return True + + def _user_has_shared_access(self, user, **kwargs): + # Nothing to check here - just pass the test + return True + + def _get_redirect_url(self, *args, **kwargs): + return reverse(self._REDIRECT_URL) + +class NewTeamView(BaseTeamView): + _FORM_CLS = NewTeamModalForm + _MSG_SUCCESS = TEAM_ADDED + +class EditTeamView(BaseTeamView): + _FORM_CLS = EditTeamModalForm + _MSG_SUCCESS = TEAM_EDITED + + def _user_has_permission(self, user, **kwargs): + team = get_object_or_404(Team, id=kwargs.get("id")) + user_is_admin = team.is_user_admin(user) + if not user_is_admin: + # If user is not an admin, we act as if there is no such team on the database + raise Http404() + return user_is_admin + + +class RemoveTeamView(BaseTeamView): + _FORM_CLS = RemoveTeamModalForm + _MSG_SUCCESS = TEAM_REMOVED + + def _user_has_permission(self, user, **kwargs): + team_id = kwargs.get("id") + team = get_object_or_404(Team, id=team_id) + user_is_admin = team.is_user_admin(user) + if not user_is_admin: + raise Http404() + return True + +class LeaveTeamView(BaseTeamView): + _FORM_CLS = LeaveTeamModalForm + _MSG_SUCCESS = TEAM_LEFT + + def _user_has_shared_access(self, user, **kwargs): + team_id = kwargs.get("id") + team = get_object_or_404(self._MODEL_CLS, id=team_id) + is_user_team_member = team.users.filter(id=user.id).exists() + if not is_user_team_member: + raise Http404() + return True diff --git a/user/views/users.py b/user/views/users.py new file mode 100644 index 00000000..eca1b2cb --- /dev/null +++ b/user/views/users.py @@ -0,0 +1,81 @@ +from django.contrib import messages +from django.contrib.auth.mixins import LoginRequiredMixin + +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.views.modal import AbstractBaseView, AbstractModalFormView +from user.forms.modals.user import UserContactForm +from user.forms.user import UserNotificationForm +from user.models import User +from django.http import HttpRequest +from django.shortcuts import render, redirect +from django.utils.translation import gettext_lazy as _ + +from konova.contexts import BaseContext + + +class UserBaseView(AbstractBaseView): + def _user_has_shared_access(self, user, **kwargs): + return True + + def _user_has_permission(self, user, **kwargs): + return True + + +class UserDetailView(LoginRequiredMixin, UserBaseView): + _TEMPLATE = "user/index.html" + _TAB_TITLE = _("User settings") + + def get(self, request: HttpRequest): + context = { + "user": request.user, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + +class NotificationsView(LoginRequiredMixin, UserBaseView): + _TEMPLATE = "user/notifications.html" + _TAB_TITLE = _("User notifications") + + def get(self, request: HttpRequest): + user = request.user + form = UserNotificationForm(user=user, data=None) + context = { + "user": user, + "form": form, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + def post(self, request: HttpRequest): + user = request.user + form = UserNotificationForm(user=user, data=request.POST) + if form.is_valid(): + form.save() + messages.success( + request, + _("Notifications edited") + ) + return redirect("user:detail") + context = { + "user": user, + "form": form, + TAB_TITLE_IDENTIFIER: self._TAB_TITLE, + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + +class ContactView(LoginRequiredMixin, AbstractModalFormView): + _FORM_CLS = UserContactForm + _MODEL_CLS = User + + def _user_has_shared_access(self, user, **kwargs): + # No specific constraints + return True + + def _user_has_permission(self, user, **kwargs): + # No specific constraints + return True diff --git a/user/views/views.py b/user/views/views.py deleted file mode 100644 index 5d773926..00000000 --- a/user/views/views.py +++ /dev/null @@ -1,201 +0,0 @@ -from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.urls import reverse - -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from user.forms.modals.team import NewTeamModalForm, EditTeamModalForm, RemoveTeamModalForm, LeaveTeamModalForm -from user.forms.modals.user import UserContactForm -from user.forms.team import TeamDataForm -from user.forms.user import UserNotificationForm -from user.models import User, Team -from django.http import HttpRequest, Http404 -from django.shortcuts import render, redirect, get_object_or_404 -from django.utils.translation import gettext_lazy as _ - -from konova.contexts import BaseContext -from konova.decorators import any_group_check, login_required_modal - - -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ Renders the user's data index view - - 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) - - -@login_required -@any_group_check -def notifications_view(request: HttpRequest): - """ Renders the notifications settings view - - Args: - request (): - - Returns: - - """ - template = "user/notifications.html" - user = request.user - - form = UserNotificationForm(user=user, data=request.POST or None) - if request.method == "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) - - -@login_required_modal -@login_required -def contact_view(request: HttpRequest, id: str): - """ Renders contact modal view of a users contact data - - Args: - request (HttpRequest): The incoming request - id (str): The user's id - - Returns: - - """ - user = get_object_or_404(User, id=id) - form = UserContactForm(request.POST or None, instance=user, request=request) - template = "modal/modal_form.html" - context = { - "form": form, - } - context = BaseContext(request, context).context - return render( - request, - template, - context - ) - - -@login_required_modal -@login_required -def data_team_view(request: HttpRequest, id: str): - """ Renders team data - - Args: - request (HttpRequest): The incoming request - id (str): The team's id - - Returns: - - """ - team = get_object_or_404(Team, id=id) - form = TeamDataForm(request.POST or None, instance=team, request=request) - template = "modal/modal_form.html" - context = { - "form": form, - } - context = BaseContext(request, context).context - return render( - request, - template, - context - ) - - -@login_required -def index_team_view(request: HttpRequest): - template = "user/team/index.html" - user = request.user - context = { - "teams": user.shared_teams, - "tab_title": _("Teams"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required_modal -@login_required -def new_team_view(request: HttpRequest): - form = NewTeamModalForm(request.POST or None, request=request) - return form.process_request( - request, - _("New team added"), - redirect_url=reverse("user:team-index") - ) - - -@login_required_modal -@login_required -def edit_team_view(request: HttpRequest, id: str): - team = get_object_or_404(Team, id=id) - user_is_admin = team.is_user_admin(request.user) - if not user_is_admin: - raise Http404() - form = EditTeamModalForm(request.POST or None, instance=team, request=request) - return form.process_request( - request, - _("Team edited"), - redirect_url=reverse("user:team-index") - ) - - -@login_required_modal -@login_required -def remove_team_view(request: HttpRequest, id: str): - team = get_object_or_404(Team, id=id) - user_is_admin = team.is_user_admin(request.user) - if not user_is_admin: - raise Http404() - form = RemoveTeamModalForm(request.POST or None, instance=team, request=request) - return form.process_request( - request, - _("Team removed"), - redirect_url=reverse("user:team-index") - ) - - -@login_required_modal -@login_required -def leave_team_view(request: HttpRequest, id: str): - team = get_object_or_404(Team, id=id) - user = request.user - - is_user_team_member = team.users.filter(id=user.id).exists() - if not is_user_team_member: - messages.info( - request, - _("You are not a member of this team") - ) - return redirect("user:team-index") - - form = LeaveTeamModalForm(request.POST or None, instance=team, request=request) - return form.process_request( - request, - _("Left Team"), - redirect_url=reverse("user:team-index") - )