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/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 257eaf8b..f2bae760 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -6,29 +6,26 @@ 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.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.decorators import shared_access_required, default_group_required, login_required_modal from konova.forms.modals import RemoveModalForm -from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ +from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ RECORDED_BLOCKS_EDIT, PARAMS_INVALID -from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ - BaseEditSpatialLocatedObjectFormView +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 -class CompensationIndexView(LoginRequiredMixin, BaseIndexView): +class CompensationIndexView(LoginRequiredMixin, AbstractIndexView): _TAB_TITLE = _("Compensations - Overview") _INDEX_TABLE_CLS = CompensationTable @@ -42,7 +39,7 @@ class CompensationIndexView(LoginRequiredMixin, BaseIndexView): return qs -class NewCompensationFormView(BaseNewSpatialLocatedObjectFormView): +class NewCompensationFormView(AbstractNewGeometryFormView): _FORM_CLS = NewCompensationForm _MODEL_CLS = Compensation _TEMPLATE = "compensation/form/view.html" @@ -82,7 +79,7 @@ class NewCompensationFormView(BaseNewSpatialLocatedObjectFormView): return super().dispatch(request, *args, **kwargs) -class EditCompensationFormView(BaseEditSpatialLocatedObjectFormView): +class EditCompensationFormView(AbstractEditGeometryFormView): _MODEL_CLS = Compensation _FORM_CLS = EditCompensationForm _TEMPLATE = "compensation/form/view.html" @@ -93,7 +90,7 @@ class EditCompensationFormView(BaseEditSpatialLocatedObjectFormView): return user.is_default_user() -class CompensationIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): +class CompensationIdentifierGeneratorView(LoginRequiredMixin, AbstractIdentifierGeneratorView): _MODEL_CLS = Compensation _REDIRECT_URL = "compensation:index" diff --git a/compensation/views/compensation/report.py b/compensation/views/compensation/report.py index dde16ea0..3d869d14 100644 --- a/compensation/views/compensation/report.py +++ b/compensation/views/compensation/report.py @@ -10,10 +10,10 @@ from django.urls import reverse from compensation.models import Compensation from konova.sub_settings.django_settings import BASE_URL from konova.utils.qrcode import QrCode -from konova.views.report import BaseReportView +from konova.views.report import AbstractReportView -class BaseCompensationReportView(BaseReportView): +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") diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 9a40b0b1..5351e5e3 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -5,31 +5,24 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib import messages -from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, redirect, render -from django.urls import reverse +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, login_required_modal -from konova.forms import SimpleGeomForm -from konova.settings import ETS_GROUP -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \ - IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE -from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ - BaseEditSpatialLocatedObjectFormView +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 -class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView): +class EcoAccountIndexView(LoginRequiredMixin, AbstractIndexView): + """ View class for indexing eco accounts + + """ _INDEX_TABLE_CLS = EcoAccountTable _TAB_TITLE = _("Eco-account - Overview") @@ -42,7 +35,12 @@ class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView): return qs -class NewEcoAccountFormView(BaseNewSpatialLocatedObjectFormView): +class NewEcoAccountFormView(AbstractNewGeometryFormView): + """ Form view class + + Renders a form for new eco accounts + + """ _FORM_CLS = NewEcoAccountForm _MODEL_CLS = EcoAccount _TEMPLATE = "compensation/form/view.html" @@ -54,7 +52,12 @@ class NewEcoAccountFormView(BaseNewSpatialLocatedObjectFormView): return user.is_default_user() -class EditEcoAccountFormView(BaseEditSpatialLocatedObjectFormView): +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" @@ -65,129 +68,20 @@ class EditEcoAccountFormView(BaseEditSpatialLocatedObjectFormView): return user.is_default_user() -@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 EcoAccountIdentifierGeneratorView(LoginRequiredMixin, AbstractIdentifierGeneratorView): + """ View class for identifier generation on 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 - ) - - 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) - - -class EcoAccountIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): _MODEL_CLS = EcoAccount _REDIRECT_URL = "compensation:acc:index" -@login_required -@default_group_required -@shared_access_required(EcoAccount, "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 - 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) - - class EcoAccountDetailView(BaseDetailView): + """ Detail view class + + Renders details of an eco account + + """ _MODEL_CLS = EcoAccount _TEMPLATE = "compensation/detail/eco_account/view.html" @@ -256,6 +150,11 @@ class EcoAccountDetailView(BaseDetailView): class RemoveEcoAccountView(LoginRequiredMixin, BaseRemoveModalFormView): + """ Form view class + + Renders a form for removing eco accounts + + """ _MODEL_CLS = EcoAccount _FORM_CLS = RemoveEcoAccountModalForm _REDIRECT_URL = "compensation:acc:index" diff --git a/compensation/views/payment.py b/compensation/views/payment.py index 57957870..a6a35637 100644 --- a/compensation/views/payment.py +++ b/compensation/views/payment.py @@ -10,10 +10,10 @@ from django.contrib.auth.mixins import LoginRequiredMixin from compensation.forms.modals.payment import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm from intervention.models import Intervention from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED -from konova.views.base import BaseModalFormView +from konova.views.modal import AbstractModalFormView -class BasePaymentView(LoginRequiredMixin, BaseModalFormView): +class BasePaymentView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = Intervention _REDIRECT_URL = "intervention:detail" diff --git a/ema/views/ema.py b/ema/views/ema.py index c211b799..7fcaa295 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -12,13 +12,14 @@ 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.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ - BaseEditSpatialLocatedObjectFormView +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 -class EmaIndexView(LoginRequiredMixin, BaseIndexView): +class EmaIndexView(LoginRequiredMixin, AbstractIndexView): _TAB_TITLE = _("EMAs - Overview") _INDEX_TABLE_CLS = EmaTable @@ -31,7 +32,7 @@ class EmaIndexView(LoginRequiredMixin, BaseIndexView): return qs -class NewEmaFormView(BaseNewSpatialLocatedObjectFormView): +class NewEmaFormView(AbstractNewGeometryFormView): _FORM_CLS = NewEmaForm _MODEL_CLS = Ema _TEMPLATE = "ema/form/view.html" @@ -43,7 +44,7 @@ class NewEmaFormView(BaseNewSpatialLocatedObjectFormView): return user.is_ets_user() -class EditEmaFormView(BaseEditSpatialLocatedObjectFormView): +class EditEmaFormView(AbstractEditGeometryFormView): _MODEL_CLS = Ema _FORM_CLS = EditEmaForm _TEMPLATE = "ema/form/view.html" @@ -55,7 +56,7 @@ class EditEmaFormView(BaseEditSpatialLocatedObjectFormView): return user.is_ets_user() -class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): +class EmaIdentifierGeneratorView(LoginRequiredMixin, AbstractIdentifierGeneratorView): _MODEL_CLS = Ema _REDIRECT_URL = "ema:index" diff --git a/intervention/views/check.py b/intervention/views/check.py index 09efc105..927e126e 100644 --- a/intervention/views/check.py +++ b/intervention/views/check.py @@ -10,10 +10,10 @@ from django.utils.translation import gettext_lazy as _ from intervention.forms.modals.check import CheckModalForm from intervention.models import Intervention -from konova.views.base import BaseModalFormView +from konova.views.modal import AbstractModalFormView -class InterventionCheckView(LoginRequiredMixin, BaseModalFormView): +class InterventionCheckView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = Intervention _FORM_CLS = CheckModalForm _MSG_SUCCESS = _("Check performed") diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index 1350340a..6cbe7d13 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -5,29 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib import messages -from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin -from django.http import HttpRequest -from django.shortcuts import get_object_or_404, render, redirect +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 -from konova.forms import SimpleGeomForm -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ - CHECK_STATE_RESET, FORM_INVALID, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE -from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ - BaseEditSpatialLocatedObjectFormView +from konova.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 -class InterventionIndexView(LoginRequiredMixin, BaseIndexView): +class InterventionIndexView(LoginRequiredMixin, AbstractIndexView): _INDEX_TABLE_CLS = InterventionTable _TAB_TITLE = _("Interventions - Overview") @@ -42,7 +35,7 @@ class InterventionIndexView(LoginRequiredMixin, BaseIndexView): return qs -class NewInterventionFormView(BaseNewSpatialLocatedObjectFormView): +class NewInterventionFormView(AbstractNewGeometryFormView): _MODEL_CLS = Intervention _FORM_CLS = NewInterventionForm _TEMPLATE = "intervention/form/view.html" @@ -50,7 +43,7 @@ class NewInterventionFormView(BaseNewSpatialLocatedObjectFormView): _TAB_TITLE = _("New intervention") -class EditInterventionFormView(BaseEditSpatialLocatedObjectFormView): +class EditInterventionFormView(AbstractEditGeometryFormView): _MODEL_CLS = Intervention _FORM_CLS = EditInterventionForm _TEMPLATE = "intervention/form/view.html" @@ -58,7 +51,7 @@ class EditInterventionFormView(BaseEditSpatialLocatedObjectFormView): _TAB_TITLE = _("Edit {}") -class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): +class InterventionIdentifierGeneratorView(LoginRequiredMixin, AbstractIdentifierGeneratorView): _MODEL_CLS = Intervention _REDIRECT_URL = "intervention:index" @@ -117,69 +110,6 @@ class InterventionDetailView(BaseDetailView): } return 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) - class RemoveInterventionView(LoginRequiredMixin, BaseRemoveModalFormView): _MODEL_CLS = Intervention _REDIRECT_URL = "intervention:index" diff --git a/intervention/views/report.py b/intervention/views/report.py index 2d676b1d..8c17f0cf 100644 --- a/intervention/views/report.py +++ b/intervention/views/report.py @@ -10,10 +10,10 @@ from django.urls import reverse from intervention.models import Intervention from konova.sub_settings.django_settings import BASE_URL from konova.utils.qrcode import QrCode -from konova.views.report import BaseReportView +from konova.views.report import AbstractReportView -class InterventionReportView(BaseReportView): +class InterventionReportView(AbstractReportView): _TEMPLATE = 'intervention/report/report.html' _MODEL = Intervention diff --git a/intervention/views/revocation.py b/intervention/views/revocation.py index 26f30c05..a67463bb 100644 --- a/intervention/views/revocation.py +++ b/intervention/views/revocation.py @@ -15,10 +15,10 @@ from intervention.forms.modals.revocation import NewRevocationModalForm, EditRev from intervention.models import Intervention, RevocationDocument from konova.utils.documents import get_document from konova.utils.message_templates import DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED, REVOCATION_ADDED -from konova.views.base import BaseModalFormView, BaseView +from konova.views.modal import AbstractModalFormView, AbstractBaseView -class BaseRevocationView(LoginRequiredMixin, BaseModalFormView): +class BaseRevocationView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = Intervention _REDIRECT_URL = "intervention:detail" @@ -48,7 +48,7 @@ class RemoveRevocationView(BaseRevocationView): _MSG_SUCCESS = REVOCATION_REMOVED -class GetRevocationDocumentView(LoginRequiredMixin, BaseView): +class GetRevocationDocumentView(LoginRequiredMixin, AbstractBaseView): _MODEL_CLS = RevocationDocument _REDIRECT_URL = "intervention:detail" diff --git a/konova/views/action.py b/konova/views/action.py index 63b44995..8047543e 100644 --- a/konova/views/action.py +++ b/konova/views/action.py @@ -11,10 +11,10 @@ from compensation.forms.modals.compensation_action import NewCompensationActionM EditCompensationActionModalForm, RemoveCompensationActionModalForm from konova.utils.message_templates import COMPENSATION_STATE_ADDED, COMPENSATION_STATE_EDITED, \ COMPENSATION_STATE_REMOVED -from konova.views.base import BaseModalFormView +from konova.views.modal import AbstractModalFormView -class AbstractCompensationActionView(LoginRequiredMixin, BaseModalFormView): +class AbstractCompensationActionView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = None _REDIRECT_URL = None diff --git a/konova/views/base.py b/konova/views/base.py index 0c7532ee..a8fc1378 100644 --- a/konova/views/base.py +++ b/konova/views/base.py @@ -5,26 +5,16 @@ Created on: 15.10.25 """ from abc import abstractmethod -from bootstrap_modal_forms.mixins import is_ajax from django.contrib import messages -from django.contrib.auth.mixins import LoginRequiredMixin -from django.core.exceptions import ObjectDoesNotExist -from django.http import HttpRequest, JsonResponse, HttpResponseRedirect -from django.shortcuts import render, redirect, get_object_or_404 +from django.http import HttpRequest +from django.shortcuts import redirect from django.urls import reverse from django.views import View -from django.utils.translation import gettext_lazy as _ - -from konova.contexts import BaseContext -from konova.forms import BaseForm, SimpleGeomForm -from konova.models import BaseObject -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.general import check_user_is_in_any_group -from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED, IDENTIFIER_REPLACED, \ - GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, RECORDED_BLOCKS_EDIT, FORM_INVALID +from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED -class BaseView(View): +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 @@ -123,519 +113,3 @@ class BaseView(View): """ return self._REDIRECT_URL_ERROR -class BaseModalFormView(BaseView): - """ 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 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 - ) - 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" - - -class BaseIndexView(BaseView): - """ Abstract base class for 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 - - -class BaseIdentifierGeneratorView(BaseView): - _MODEL_CLS = None - _REDIRECT_URL: str = "home" - - class Meta: - abstract = True - - def get(self, request: HttpRequest): - tmp_obj = self._MODEL_CLS() - identifier = tmp_obj.generate_new_identifier() - while self._MODEL_CLS.objects.filter(identifier=identifier).exists(): - identifier = tmp_obj.generate_new_identifier() - return JsonResponse( - data={ - "gen_data": identifier - } - ) - - def _user_has_permission(self, user, **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 - - -class BaseFormView(BaseView): - """ 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 BaseSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView): - """ Abstract base view for processing objects with spatial data - - """ - _GEOMETRY_FORM_CLS = SimpleGeomForm - - class Meta: - abstract = True - - -class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): - """ 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 BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView): - """ 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/deadline.py b/konova/views/deadline.py index 5258e72f..8b7c8cab 100644 --- a/konova/views/deadline.py +++ b/konova/views/deadline.py @@ -10,10 +10,10 @@ from django.contrib.auth.mixins import LoginRequiredMixin from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm from konova.forms.modals import RemoveDeadlineModalForm from konova.utils.message_templates import DEADLINE_ADDED, DEADLINE_EDITED, DEADLINE_REMOVED -from konova.views.base import BaseModalFormView +from konova.views.modal import AbstractModalFormView -class AbstractNewDeadlineView(LoginRequiredMixin, BaseModalFormView): +class AbstractNewDeadlineView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = None _FORM_CLS = NewDeadlineModalForm _REDIRECT_URL = None @@ -29,7 +29,7 @@ class AbstractNewDeadlineView(LoginRequiredMixin, BaseModalFormView): return user.is_default_user() -class AbstractEditDeadlineView(LoginRequiredMixin, BaseModalFormView): +class AbstractEditDeadlineView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = None _FORM_CLS = EditDeadlineModalForm _REDIRECT_URL = None @@ -45,7 +45,7 @@ class AbstractEditDeadlineView(LoginRequiredMixin, BaseModalFormView): return user.is_default_user() -class AbstractRemoveDeadlineView(LoginRequiredMixin, BaseModalFormView): +class AbstractRemoveDeadlineView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = None _FORM_CLS = RemoveDeadlineModalForm _REDIRECT_URL = None diff --git a/konova/views/deduction.py b/konova/views/deduction.py index 73bc9817..b549d084 100644 --- a/konova/views/deduction.py +++ b/konova/views/deduction.py @@ -11,10 +11,10 @@ from django.urls import reverse from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \ RemoveEcoAccountDeductionModalForm from konova.utils.general import check_id_is_valid_uuid -from konova.views.base import BaseModalFormView +from konova.views.modal import AbstractModalFormView -class AbstractDeductionView(BaseModalFormView): +class AbstractDeductionView(AbstractModalFormView): _REDIRECT_URL = None def dispatch(self, request, *args, **kwargs): diff --git a/konova/views/detail.py b/konova/views/detail.py index e324231f..2c786f2d 100644 --- a/konova/views/detail.py +++ b/konova/views/detail.py @@ -16,10 +16,10 @@ from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.general import check_id_is_valid_uuid from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE -from konova.views.base import BaseView +from konova.views.base import AbstractBaseView -class BaseDetailView(LoginRequiredMixin, BaseView): +class BaseDetailView(LoginRequiredMixin, AbstractBaseView): _MODEL_CLS = None class Meta: diff --git a/konova/views/document.py b/konova/views/document.py index f80e399d..ea5d1b06 100644 --- a/konova/views/document.py +++ b/konova/views/document.py @@ -12,10 +12,10 @@ from django.shortcuts import get_object_or_404 from konova.forms.modals import EditDocumentModalForm from konova.utils.documents import get_document from konova.utils.message_templates import DOCUMENT_ADDED, DOCUMENT_EDITED, DOCUMENT_REMOVED_TEMPLATE -from konova.views.base import BaseModalFormView, BaseView +from konova.views.modal import AbstractModalFormView, AbstractBaseView -class AbstractNewDocumentView(LoginRequiredMixin, BaseModalFormView): +class AbstractNewDocumentView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = None _FORM_CLS = None _REDIRECT_URL = None @@ -31,7 +31,7 @@ class AbstractNewDocumentView(LoginRequiredMixin, BaseModalFormView): return user.is_default_user() -class AbstractGetDocumentView(LoginRequiredMixin, BaseView): +class AbstractGetDocumentView(LoginRequiredMixin, AbstractBaseView): _MODEL_CLS = None _DOCUMENT_CLS = None @@ -68,7 +68,7 @@ class AbstractGetDocumentView(LoginRequiredMixin, BaseView): return obj.is_shared_with(user) -class AbstractRemoveDocumentView(LoginRequiredMixin, BaseModalFormView): +class AbstractRemoveDocumentView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = None _DOCUMENT_CLS = None _FORM_CLS = None @@ -90,7 +90,7 @@ class AbstractRemoveDocumentView(LoginRequiredMixin, BaseModalFormView): return self._MSG_SUCCESS.format(doc.title) -class AbstractEditDocumentView(LoginRequiredMixin, BaseModalFormView): +class AbstractEditDocumentView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = None _DOCUMENT_CLS = None _FORM_CLS = EditDocumentModalForm 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 1c25131f..181400b2 100644 --- a/konova/views/geometry.py +++ b/konova/views/geometry.py @@ -15,10 +15,10 @@ from konova.models import Geometry from konova.settings import GEOM_THRESHOLD_RECALCULATION_SECONDS from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.tasks import celery_update_parcels -from konova.views.base import BaseView +from konova.views.base import AbstractBaseView -class GeomParcelsView(BaseView): +class GeomParcelsView(AbstractBaseView): _TEMPLATE = "konova/includes/parcels/parcel_table_frame.html" def get(self, request: HttpRequest, id: str): @@ -114,7 +114,7 @@ class GeomParcelsView(BaseView): return True -class GeomParcelsContentView(BaseView): +class GeomParcelsContentView(AbstractBaseView): _TEMPLATE = "konova/includes/parcels/parcel_table_content.html" def get(self, request: HttpRequest, id: str, page: int): diff --git a/konova/views/home.py b/konova/views/home.py index 0c0772ea..796c36c6 100644 --- a/konova/views/home.py +++ b/konova/views/home.py @@ -15,11 +15,11 @@ from compensation.models import EcoAccount, Compensation from intervention.models import Intervention from konova.contexts import BaseContext from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.views.base import BaseView +from konova.views.base import AbstractBaseView from news.models import ServerMessage -class HomeView(LoginRequiredMixin, BaseView): +class HomeView(LoginRequiredMixin, AbstractBaseView): _TEMPLATE = "konova/home.html" def get(self, request: HttpRequest): 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 ab1a0498..0bbb2fb7 100644 --- a/konova/views/log.py +++ b/konova/views/log.py @@ -9,10 +9,10 @@ from django.shortcuts import get_object_or_404, render from django.utils.translation import gettext_lazy as _ from konova.contexts import BaseContext -from konova.views.base import BaseView +from konova.views.base import AbstractBaseView -class AbstractLogView(BaseView): +class AbstractLogView(AbstractBaseView): _MODEL_CLS = None _TEMPLATE = "modal/modal_generic.html" 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 f4dd9e19..40d41adc 100644 --- a/konova/views/record.py +++ b/konova/views/record.py @@ -7,10 +7,10 @@ Created on: 19.08.22 """ from konova.forms.modals import RecordModalForm from konova.utils.message_templates import ENTRY_RECORDED, ENTRY_UNRECORDED -from konova.views.base import BaseModalFormView +from konova.views.modal import AbstractModalFormView -class AbstractRecordView(BaseModalFormView): +class AbstractRecordView(AbstractModalFormView): _MODEL_CLS = None _FORM_CLS = RecordModalForm _MSG_SUCCESS = None diff --git a/konova/views/remove.py b/konova/views/remove.py index 32b3928a..27ee1410 100644 --- a/konova/views/remove.py +++ b/konova/views/remove.py @@ -7,11 +7,10 @@ from django.urls import reverse from konova.forms.modals import RemoveModalForm from konova.utils.message_templates import GENERIC_REMOVED_TEMPLATE -from konova.views.base import BaseModalFormView +from konova.views.modal import AbstractModalFormView -class BaseRemoveModalFormView(BaseModalFormView): - _MODEL_CLS = None +class BaseRemoveModalFormView(AbstractModalFormView): _FORM_CLS = RemoveModalForm _MSG_SUCCESS = GENERIC_REMOVED_TEMPLATE _REDIRECT_URL = None diff --git a/konova/views/report.py b/konova/views/report.py index c98b6ead..e6f0d7b1 100644 --- a/konova/views/report.py +++ b/konova/views/report.py @@ -13,10 +13,10 @@ from django.utils.translation import gettext_lazy as _ from konova.contexts import BaseContext from konova.forms import SimpleGeomForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.views.base import BaseView +from konova.views.base import AbstractBaseView -class BaseReportView(BaseView): +class AbstractReportView(AbstractBaseView): _TEMPLATE = None _TAB_TITLE = _("Report {}") _MODEL = None diff --git a/konova/views/resubmission.py b/konova/views/resubmission.py index 9cf26bd8..57698cd2 100644 --- a/konova/views/resubmission.py +++ b/konova/views/resubmission.py @@ -8,10 +8,10 @@ Created on: 19.08.22 from django.contrib.auth.mixins import LoginRequiredMixin from konova.utils.message_templates import NEW_RESUBMISSION_CREATED -from konova.views.base import BaseModalFormView +from konova.views.modal import AbstractModalFormView -class AbstractResubmissionView(LoginRequiredMixin, BaseModalFormView): +class AbstractResubmissionView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = None _FORM_CLS = None _REDIRECT_URL = None diff --git a/konova/views/share.py b/konova/views/share.py index e9757d9f..f8680eab 100644 --- a/konova/views/share.py +++ b/konova/views/share.py @@ -12,10 +12,11 @@ from django.utils.translation import gettext_lazy as _ from intervention.forms.modals.share import ShareModalForm from konova.utils.message_templates import DATA_SHARE_SET -from konova.views.base import BaseView, BaseModalFormView +from konova.views.base import AbstractBaseView +from konova.views.modal import AbstractModalFormView -class AbstractShareByTokenView(LoginRequiredMixin, BaseView): +class AbstractShareByTokenView(LoginRequiredMixin, AbstractBaseView): _MODEL_CLS = None _REDIRECT_URL = None @@ -69,7 +70,7 @@ class AbstractShareByTokenView(LoginRequiredMixin, BaseView): return True -class AbstractShareFormView(LoginRequiredMixin, BaseModalFormView): +class AbstractShareFormView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = None _FORM_CLS = ShareModalForm _MSG_SUCCESS = DATA_SHARE_SET diff --git a/konova/views/state.py b/konova/views/state.py index 4170aef0..55da9319 100644 --- a/konova/views/state.py +++ b/konova/views/state.py @@ -12,10 +12,10 @@ from compensation.forms.modals.state import NewCompensationStateModalForm, EditC RemoveCompensationStateModalForm from konova.utils.message_templates import COMPENSATION_STATE_ADDED, COMPENSATION_STATE_EDITED, \ COMPENSATION_STATE_REMOVED -from konova.views.base import BaseModalFormView +from konova.views.modal import AbstractModalFormView -class AbstractCompensationStateView(LoginRequiredMixin, BaseModalFormView): +class AbstractCompensationStateView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = None _FORM_CLS = None _REDIRECT_URL = None diff --git a/user/views/api_token.py b/user/views/api_token.py index f90729ab..125b9e78 100644 --- a/user/views/api_token.py +++ b/user/views/api_token.py @@ -15,7 +15,7 @@ from konova.contexts import BaseContext from konova.decorators import default_group_required from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.message_templates import NEW_API_TOKEN_GENERATED -from konova.views.base import BaseModalFormView +from konova.views.modal import AbstractModalFormView from user.forms.modals.api_token import NewAPITokenModalForm from user.models import User @@ -38,7 +38,7 @@ class APITokenView(View): context = BaseContext(request, context).context return render(request, template, context) -class NewAPITokenView(LoginRequiredMixin, BaseModalFormView): +class NewAPITokenView(LoginRequiredMixin, AbstractModalFormView): _MODEL_CLS = User _FORM_CLS = NewAPITokenModalForm _MSG_SUCCESS = NEW_API_TOKEN_GENERATED diff --git a/user/views/teams.py b/user/views/teams.py index bde72b93..d9be7fe2 100644 --- a/user/views/teams.py +++ b/user/views/teams.py @@ -11,14 +11,14 @@ from django.utils.translation import gettext_lazy as _ from konova.contexts import BaseContext from konova.utils.message_templates import TEAM_LEFT, TEAM_REMOVED, TEAM_EDITED, TEAM_ADDED -from konova.views.base import BaseModalFormView +from 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, BaseModalFormView): +class TeamDetailModalView(LoginRequiredMixin, AbstractModalFormView): _FORM_CLS = TeamDataForm _MODEL_CLS = Team @@ -45,7 +45,7 @@ class TeamIndexView(LoginRequiredMixin, UserBaseView): return render(request, self._TEMPLATE, context) -class BaseTeamView(LoginRequiredMixin, BaseModalFormView): +class BaseTeamView(LoginRequiredMixin, AbstractModalFormView): _REDIRECT_URL = "user:team-index" _MODEL_CLS = Team diff --git a/user/views/users.py b/user/views/users.py index 186eee47..eca1b2cb 100644 --- a/user/views/users.py +++ b/user/views/users.py @@ -2,7 +2,7 @@ from django.contrib import messages from django.contrib.auth.mixins import LoginRequiredMixin from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.views.base import BaseView, BaseModalFormView +from konova.views.modal import AbstractBaseView, AbstractModalFormView from user.forms.modals.user import UserContactForm from user.forms.user import UserNotificationForm from user.models import User @@ -13,7 +13,7 @@ from django.utils.translation import gettext_lazy as _ from konova.contexts import BaseContext -class UserBaseView(BaseView): +class UserBaseView(AbstractBaseView): def _user_has_shared_access(self, user, **kwargs): return True @@ -68,7 +68,7 @@ class NotificationsView(LoginRequiredMixin, UserBaseView): return render(request, self._TEMPLATE, context) -class ContactView(LoginRequiredMixin, BaseModalFormView): +class ContactView(LoginRequiredMixin, AbstractModalFormView): _FORM_CLS = UserContactForm _MODEL_CLS = User