# Renaming

* renames certain classes to match their content
* splits larger files into smaller ones
This commit is contained in:
mpeltriaux 2025-12-12 14:13:24 +01:00
parent c4cd40913d
commit e49eed21da
32 changed files with 705 additions and 815 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

282
konova/views/form.py Normal file
View File

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

View File

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

View File

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

View File

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

65
konova/views/index.py Normal file
View File

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

View File

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

183
konova/views/modal.py Normal file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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