Compare commits

...

4 Commits

Author SHA1 Message Date
d85ebccec8 # Compensation State view refactoring
* refactors compensation state views for kom, ema, oek
* updates tests
* refactors before-after state toggling into initialization of NewCompensationStateModalForm
2025-10-21 21:05:45 +02:00
837c1de938 # Remove View refactoring
* refactors remove view for kom, eiv, oek and ema
* introduces BaseRemoveModalFormView
* moves html blocking logic from BaseModalForm into BaseModalFormView
2025-10-21 20:28:43 +02:00
fe2ac3d97d # Test update
* fixes bug for sharing via token where permission was too tight
2025-10-21 19:37:34 +02:00
554ade6794 # Share view refactoring
* refactors share views for eiv, oek, ema (kom does not have any)
2025-10-21 19:15:17 +02:00
27 changed files with 249 additions and 475 deletions

View File

@ -5,21 +5,17 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from bootstrap_modal_forms.mixins import is_ajax
from dal import autocomplete
from django import forms
from django.contrib import messages
from django.http import HttpResponseRedirect, HttpRequest
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from compensation.models import CompensationState
from intervention.inputs import CompensationStateTreeRadioSelect
from konova.contexts import BaseContext
from konova.forms.modals import RemoveModalForm, BaseModalForm
from konova.utils.message_templates import COMPENSATION_STATE_EDITED, FORM_INVALID, ADDED_COMPENSATION_STATE
from konova.utils.message_templates import COMPENSATION_STATE_EDITED, ADDED_COMPENSATION_STATE
class NewCompensationStateModalForm(BaseModalForm):
@ -68,10 +64,13 @@ class NewCompensationStateModalForm(BaseModalForm):
)
)
_is_before_state: bool = False
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New state")
self.form_caption = _("Insert data for the new state")
self._is_before_state = bool(self.request.GET.get("before", False))
choices = KonovaCode.objects.filter(
code_lists__in=[CODELIST_BIOTOPES_ID],
is_archived=False,
@ -83,65 +82,19 @@ class NewCompensationStateModalForm(BaseModalForm):
]
self.fields["biotope_type"].choices = choices
def save(self, is_before_state: bool = False):
state = self.instance.add_state(self, is_before_state)
def save(self):
state = self.instance.add_state(self, self._is_before_state)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_STATE)
return state
def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
""" Generic processing of request
Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used
+++
The generic method from super class can not be used, since we need to do some request parameter check in here.
+++
Args:
request (HttpRequest): The incoming request
msg_success (str): The message in case of successful removing
msg_error (str): The message in case of an error
Returns:
"""
redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home")
template = self.template
if request.method == "POST":
if self.is_valid():
# Modal forms send one POST for checking on data validity. This can be used to return possible errors
# on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
# saving/commiting of the data to the database. is_ajax() performs this check. The first request is
# an ajax call, the second is a regular form POST.
if not is_ajax(request.META):
is_before_state = bool(request.GET.get("before", False))
self.save(is_before_state=is_before_state)
messages.success(
request,
msg_success
)
return HttpResponseRedirect(redirect_url)
else:
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
elif request.method == "GET":
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
else:
raise NotImplementedError
class EditCompensationStateModalForm(NewCompensationStateModalForm):
state = None
def __init__(self, *args, **kwargs):
self.state = kwargs.pop("state", None)
state_id = kwargs.pop("state_id", None)
self.state = CompensationState.objects.get(id=state_id)
super().__init__(*args, **kwargs)
self.form_title = _("Edit state")
biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None
@ -172,8 +125,8 @@ class RemoveCompensationStateModalForm(RemoveModalForm):
state = None
def __init__(self, *args, **kwargs):
state = kwargs.pop("state", None)
self.state = state
state_id = kwargs.pop("state_id", None)
self.state = CompensationState.objects.get(id=state_id)
super().__init__(*args, **kwargs)
def save(self):

View File

@ -190,12 +190,20 @@ class NewCompensationStateModalFormTestCase(BaseTestCase):
self.assertEqual(self.compensation.before_states.count(), 0)
self.assertEqual(self.compensation.after_states.count(), 0)
form = NewCompensationStateModalForm(data, request=self.request, instance=self.compensation)
self.request.GET._mutable = True
self.request.GET.update(
{
"before": True,
}
)
self.request.GET._mutable = False
form = NewCompensationStateModalForm(
data,
request=self.request,
instance=self.compensation,
)
self.assertTrue(form.is_valid(), msg=form.errors)
is_before_state = True
state = form.save(is_before_state)
state = form.save()
self.assertEqual(self.compensation.before_states.count(), 1)
self.assertEqual(self.compensation.after_states.count(), 0)
@ -209,8 +217,16 @@ class NewCompensationStateModalFormTestCase(BaseTestCase):
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, ADDED_COMPENSATION_STATE)
is_before_state = False
state = form.save(is_before_state)
self.request.GET._mutable = True
del self.request.GET["before"]
self.request.GET._mutable = False
form = NewCompensationStateModalForm(
data,
request=self.request,
instance=self.compensation,
)
self.assertTrue(form.is_valid(), msg=form.errors)
state = form.save()
self.assertEqual(self.compensation.before_states.count(), 1)
self.assertEqual(self.compensation.after_states.count(), 1)
@ -234,7 +250,11 @@ class EditCompensationStateModalFormTestCase(NewCompensationStateModalFormTestCa
self.compensation.after_states.add(self.comp_state)
def test_init(self):
form = EditCompensationStateModalForm(request=self.request, instance=self.compensation, state=self.comp_state)
form = EditCompensationStateModalForm(
request=self.request,
instance=self.compensation,
state_id=self.comp_state.id
)
self.assertEqual(form.state, self.comp_state)
self.assertEqual(form.form_title, str(_("Edit state")))
@ -265,7 +285,7 @@ class EditCompensationStateModalFormTestCase(NewCompensationStateModalFormTestCa
data,
request=self.request,
instance=self.compensation,
state=self.comp_state
state_id=self.comp_state.id
)
self.assertTrue(form.is_valid(), msg=form.errors)
@ -286,7 +306,11 @@ class RemoveCompensationStateModalFormTestCase(EditCompensationStateModalFormTes
super().setUp()
def test_init(self):
form = RemoveCompensationStateModalForm(request=self.request, instance=self.compensation, state=self.comp_state)
form = RemoveCompensationStateModalForm(
request=self.request,
instance=self.compensation,
state_id=self.comp_state.id
)
self.assertEqual(form.state, self.comp_state)
@ -298,7 +322,7 @@ class RemoveCompensationStateModalFormTestCase(EditCompensationStateModalFormTes
data,
request=self.request,
instance=self.compensation,
state=self.comp_state
state_id=self.comp_state.id
)
self.assertTrue(form.is_valid(), msg=form.errors)

View File

@ -18,8 +18,8 @@ from compensation.views.compensation.action import NewCompensationActionView, Ed
from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \
RemoveCompensationStateView
from compensation.views.compensation.compensation import \
remove_view, CompensationIndexView, CompensationIdentifierGeneratorView, CompensationDetailView, \
NewCompensationFormView, EditCompensationFormView
CompensationIndexView, CompensationIdentifierGeneratorView, CompensationDetailView, \
NewCompensationFormView, EditCompensationFormView, RemoveCompensationView
from compensation.views.compensation.log import CompensationLogView
urlpatterns = [
@ -31,7 +31,7 @@ urlpatterns = [
path('<id>', CompensationDetailView.as_view(), name='detail'),
path('<id>/log', CompensationLogView.as_view(), name='log'),
path('<id>/edit', EditCompensationFormView.as_view(), name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/remove', RemoveCompensationView.as_view(), name='remove'),
path('<id>/state/new', NewCompensationStateView.as_view(), name='new-state'),
path('<id>/state/<state_id>/edit', EditCompensationStateView.as_view(), name='state-edit'),

View File

@ -8,9 +8,8 @@ Created on: 24.08.21
from django.urls import path
from compensation.autocomplete.eco_account import EcoAccountAutocomplete
from compensation.views.eco_account.eco_account import remove_view, \
EcoAccountIndexView, EcoAccountIdentifierGeneratorView, EcoAccountDetailView, NewEcoAccountFormView, \
EditEcoAccountFormView
from compensation.views.eco_account.eco_account import EcoAccountIndexView, EcoAccountIdentifierGeneratorView, \
EcoAccountDetailView, NewEcoAccountFormView, EditEcoAccountFormView, RemoveEcoAccountView
from compensation.views.eco_account.log import EcoAccountLogView
from compensation.views.eco_account.record import EcoAccountRecordView
from compensation.views.eco_account.report import EcoAccountReportView
@ -37,7 +36,7 @@ urlpatterns = [
path('<id>/record', EcoAccountRecordView.as_view(), name='record'),
path('<id>/report', EcoAccountReportView.as_view(), name='report'),
path('<id>/edit', EditEcoAccountFormView.as_view(), name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/remove', RemoveEcoAccountView.as_view(), name='remove'),
path('<id>/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'),
path('<id>/state/new', NewEcoAccountStateView.as_view(), name='new-state'),

View File

@ -25,6 +25,7 @@ from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_C
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \
BaseEditSpatialLocatedObjectFormView
from konova.views.detail import BaseDetailView
from konova.views.remove import BaseRemoveModalFormView
class CompensationIndexView(LoginRequiredMixin, BaseIndexView):
@ -164,25 +165,10 @@ class CompensationDetailView(BaseDetailView):
return context
@login_required_modal
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request=request,
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
redirect_url=reverse("compensation:index"),
)
class RemoveCompensationView(LoginRequiredMixin, BaseRemoveModalFormView):
_MODEL_CLS = Compensation
_FORM_CLS = RemoveModalForm
_REDIRECT_URL = "compensation:index"
def _user_has_permission(self, user):
return user.is_default_user()

View File

@ -5,46 +5,21 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \
AbstractRemoveCompensationStateView
class NewCompensationStateView(AbstractNewCompensationStateView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Compensation
_REDIRECT_URL = "compensation:detail"
class EditCompensationStateView(AbstractEditCompensationStateView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Compensation
_REDIRECT_URL = "compensation:detail"
class RemoveCompensationStateView(AbstractRemoveCompensationStateView):
model = Compensation
redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Compensation
_REDIRECT_URL = "compensation:detail"

View File

@ -22,6 +22,10 @@ class NewEcoAccountDeductionView(LoginRequiredMixin, AbstractNewDeductionView):
if not obj.recorded:
raise Http404()
def _check_for_recorded_instance(self, obj):
# Deductions can be created on recorded as well as on non-recorded entries
return None
class EditEcoAccountDeductionView(LoginRequiredMixin, AbstractEditDeductionView):
_MODEL_CLS = EcoAccount

View File

@ -26,6 +26,7 @@ from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECO
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \
BaseEditSpatialLocatedObjectFormView
from konova.views.detail import BaseDetailView
from konova.views.remove import BaseRemoveModalFormView
class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView):
@ -254,34 +255,10 @@ class EcoAccountDetailView(BaseDetailView):
return context
@login_required_modal
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the eco account
Args:
request (HttpRequest): The incoming request
id (str): The account's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
# If the eco account has already been recorded OR there are already deductions, it can not be deleted by a regular
# default group user
if acc.recorded is not None or acc.deductions.exists():
user = request.user
if not user.in_group(ETS_GROUP):
messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED)
return redirect("compensation:acc:detail", id=id)
form = RemoveEcoAccountModalForm(request.POST or None, instance=acc, request=request)
return form.process_request(
request=request,
msg_success=_("Eco-account removed"),
redirect_url=reverse("compensation:acc:index"),
)
class RemoveEcoAccountView(LoginRequiredMixin, BaseRemoveModalFormView):
_MODEL_CLS = EcoAccount
_FORM_CLS = RemoveEcoAccountModalForm
_REDIRECT_URL = "compensation:acc:index"
def _user_has_permission(self, user):
return user.is_default_user()

View File

@ -5,29 +5,15 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.share import AbstractShareByTokenView, AbstractShareFormView
class EcoAccountShareByTokenView(AbstractShareByTokenView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = EcoAccount
_REDIRECT_URL = "compensation:acc:detail"
class EcoAccountShareFormView(AbstractShareFormView):
model = EcoAccount
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = EcoAccount
_REDIRECT_URL = "compensation:acc:detail"

View File

@ -5,46 +5,21 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \
AbstractRemoveCompensationStateView
class NewEcoAccountStateView(AbstractNewCompensationStateView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = EcoAccount
_REDIRECT_URL = "compensation:acc:detail"
class EditEcoAccountStateView(AbstractEditCompensationStateView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = EcoAccount
_REDIRECT_URL = "compensation:acc:detail"
class RemoveEcoAccountStateView(AbstractRemoveCompensationStateView):
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = EcoAccount
_REDIRECT_URL = "compensation:acc:detail"

View File

@ -10,8 +10,8 @@ from django.urls import path
from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView
from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView
from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView
from ema.views.ema import remove_view, EmaIndexView, \
EmaIdentifierGeneratorView, EmaDetailView, EditEmaFormView, NewEmaFormView
from ema.views.ema import EmaIndexView, EmaIdentifierGeneratorView, EmaDetailView, EditEmaFormView, NewEmaFormView, \
RemoveEmaView
from ema.views.log import EmaLogView
from ema.views.record import EmaRecordView
from ema.views.report import EmaReportView
@ -27,7 +27,7 @@ urlpatterns = [
path("<id>", EmaDetailView.as_view(), name="detail"),
path('<id>/log', EmaLogView.as_view(), name='log'),
path('<id>/edit', EditEmaFormView.as_view(), name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/remove', RemoveEmaView.as_view(), name='remove'),
path('<id>/record', EmaRecordView.as_view(), name='record'),
path('<id>/report', EmaReportView.as_view(), name='report'),
path('<id>/resub', EmaResubmissionView.as_view(), name='resubmission-create'),

View File

@ -5,21 +5,17 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.urls import reverse
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.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.forms.modals import RemoveModalForm
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \
BaseEditSpatialLocatedObjectFormView
from konova.views.detail import BaseDetailView
from konova.views.remove import BaseRemoveModalFormView
class EmaIndexView(LoginRequiredMixin, BaseIndexView):
@ -112,26 +108,9 @@ class EmaDetailView(BaseDetailView):
}
return context
class RemoveEmaView(LoginRequiredMixin, BaseRemoveModalFormView):
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:index"
@login_required_modal
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = RemoveModalForm(request.POST or None, instance=ema, request=request)
return form.process_request(
request=request,
msg_success=_("EMA removed"),
redirect_url=reverse("ema:index"),
)
def _user_has_permission(self, user):
return user.is_ets_user()

View File

@ -5,29 +5,17 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema
from konova.decorators import conservation_office_group_required, shared_access_required, login_required_modal
from konova.views.share import AbstractShareByTokenView, AbstractShareFormView
class EmaShareByTokenView(AbstractShareByTokenView):
model = Ema
redirect_url = "ema:detail"
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:detail"
class EmaShareFormView(AbstractShareFormView):
model = Ema
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user):
return user.is_ets_user()

View File

@ -5,46 +5,30 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema
from konova.decorators import conservation_office_group_required, shared_access_required, login_required_modal
from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \
AbstractRemoveCompensationStateView
class NewEmaStateView(AbstractNewCompensationStateView):
model = Ema
redirect_url = "ema:detail"
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user):
return user.is_ets_user()
class EditEmaStateView(AbstractEditCompensationStateView):
model = Ema
redirect_url = "ema:detail"
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user):
return user.is_ets_user()
class RemoveEmaStateView(AbstractRemoveCompensationStateView):
model = Ema
redirect_url = "ema:detail"
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user):
return user.is_ets_user()

View File

@ -14,9 +14,8 @@ from intervention.views.deduction import NewInterventionDeductionView, EditInter
RemoveInterventionDeductionView
from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \
RemoveInterventionDocumentView, EditInterventionDocumentView
from intervention.views.intervention import remove_view, \
InterventionIndexView, InterventionIdentifierGeneratorView, InterventionDetailView, NewInterventionFormView, \
EditInterventionFormView
from intervention.views.intervention import InterventionIndexView, InterventionIdentifierGeneratorView, \
InterventionDetailView, NewInterventionFormView, EditInterventionFormView, RemoveInterventionView
from intervention.views.log import InterventionLogView
from intervention.views.record import InterventionRecordView
from intervention.views.report import InterventionReportView
@ -33,7 +32,7 @@ urlpatterns = [
path('<id>', InterventionDetailView.as_view(), name='detail'),
path('<id>/log', InterventionLogView.as_view(), name='log'),
path('<id>/edit', EditInterventionFormView.as_view(), name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/remove', RemoveInterventionView.as_view(), name='remove'),
path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'),
path('<id>/share', InterventionShareFormView.as_view(), name='share-form'),
path('<id>/check', InterventionCheckView.as_view(), name='check'),

View File

@ -10,22 +10,21 @@ 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.urls import reverse
from django.utils.translation import gettext_lazy as _
from intervention.forms.intervention import EditInterventionForm, NewInterventionForm
from intervention.models import Intervention
from intervention.tables import InterventionTable
from konova.contexts import BaseContext
from konova.decorators import default_group_required, shared_access_required, login_required_modal
from konova.decorators import default_group_required, shared_access_required
from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
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.views.detail import BaseDetailView
from konova.views.remove import BaseRemoveModalFormView
class InterventionIndexView(LoginRequiredMixin, BaseIndexView):
@ -181,26 +180,6 @@ def edit_view(request: HttpRequest, id: str):
context = BaseContext(request, context).context
return render(request, template, context)
@login_required_modal
@login_required
@default_group_required
@shared_access_required(Intervention, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a remove view for this intervention
Args:
request (HttpRequest): The incoming request
id (str): The uuid id as string
Returns:
"""
obj = Intervention.objects.get(id=id)
identifier = obj.identifier
form = RemoveModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
_("{} removed").format(identifier),
redirect_url=reverse("intervention:index")
)
class RemoveInterventionView(LoginRequiredMixin, BaseRemoveModalFormView):
_MODEL_CLS = Intervention
_REDIRECT_URL = "intervention:index"

View File

@ -5,29 +5,15 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from intervention.models import Intervention
from konova.decorators import default_group_required, shared_access_required, login_required_modal
from konova.views.share import AbstractShareByTokenView, AbstractShareFormView
class InterventionShareByTokenView(AbstractShareByTokenView):
model = Intervention
redirect_url = "intervention:detail"
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Intervention
_REDIRECT_URL = "intervention:detail"
class InterventionShareFormView(AbstractShareFormView):
model = Intervention
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
_MODEL_CLS = Intervention
_REDIRECT_URL = "intervention:detail"

View File

@ -10,14 +10,11 @@ from abc import abstractmethod
from django import forms
from django.utils.translation import gettext_lazy as _
from konova.models import BaseObject
class BaseForm(forms.Form):
"""
Basic form for that holds attributes needed in all other forms
"""
template = None
action_url = None
action_btn_label = _("Save")
form_title = None
@ -43,7 +40,6 @@ class BaseForm(forms.Form):
self.has_required_fields = True
break
self.check_for_recorded_instance()
self.__check_valid_label_input_ratio()
@abstractmethod
@ -137,34 +133,3 @@ class BaseForm(forms.Form):
set_class = self.fields[field].widget.attrs.get("class", "")
set_class = set_class.replace(cls, "")
self.fields[field].widget.attrs["class"] = set_class
def check_for_recorded_instance(self):
""" Checks if the instance is recorded and runs some special logic if yes
If the instance is recorded, the form shall not display any possibility to
edit any data. Instead, the users should get some information about why they can not edit anything.
There are situations where the form should be rendered regularly,
e.g deduction forms for (recorded) eco accounts.
Returns:
"""
is_none = self.instance is None
is_other_data_type = not isinstance(self.instance, BaseObject)
if is_none or is_other_data_type:
# Do nothing
return
if self.instance.is_recorded:
self.block_form()
def block_form(self):
"""
Overwrites template, providing no actions
Returns:
"""
self.template = "form/recorded_no_edit.html"

View File

@ -23,7 +23,7 @@ class BaseModalForm(BaseForm, BSModalForm):
"""
is_modal_form = True
render_submit = True
template = "modal/modal_form.html"
_TEMPLATE = "modal/modal_form.html"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
@ -43,7 +43,7 @@ class BaseModalForm(BaseForm, BSModalForm):
"""
redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home")
template = self.template
template = self._TEMPLATE
if request.method == "POST":
if self.is_valid():
if not is_ajax(request.META):

View File

@ -543,7 +543,7 @@ class BaseViewTestCase(BaseTestCase):
for url, redirect_to in urls.items():
response = client.get(url, follow=True)
# Expect redirects to the landing page
self.assertEqual(response.redirect_chain[0], (redirect_to, 302), msg=f"Failed for {url}")
self.assertEqual(response.redirect_chain[0], (redirect_to, 302), msg=f"Failed for {url}. Expected {redirect_to}")
def assert_url_fail(self, client: Client, urls: list):
""" Assert for all given urls a direct 302 response

View File

@ -20,6 +20,9 @@ ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office us
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
CHECK_STATE_RESET = _("Status of Checked reset")
# REMOVED
GENERIC_REMOVED_TEMPLATE = _("{} removed")
# RECORDING
RECORDED_BLOCKS_EDIT = _("Entry is recorded. To edit data, the entry first needs to be unrecorded.")
ENTRY_RECORDED = _("{} recorded")

View File

@ -16,8 +16,9 @@ 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, check_id_is_valid_uuid
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
@ -89,6 +90,7 @@ class BaseModalFormView(BaseView):
def get(self, request: HttpRequest, id: str, *args, **kwargs):
obj = self._MODEL_CLS.objects.get(id=id)
self._check_for_recorded_instance(obj)
form = self._FORM_CLS(
request.POST or None,
request.FILES or None,
@ -104,6 +106,7 @@ class BaseModalFormView(BaseView):
def post(self, request: HttpRequest, id: str, *args, **kwargs):
obj = self._MODEL_CLS.objects.get(id=id)
self._check_for_recorded_instance(obj)
form = self._FORM_CLS(
request.POST or None,
request.FILES or None,
@ -140,6 +143,36 @@ class BaseModalFormView(BaseView):
def _get_msg_success(self, *args, **kwargs):
return self._MSG_SUCCESS
def _check_for_recorded_instance(self, obj):
""" Checks if the object on this view is recorded and runs some special logic if yes
If the instance is recorded, the view should provide some information about why the user can not edit anything.
There are situations where the form should be rendered regularly,
e.g deduction forms for (recorded) eco accounts.
Returns:
"""
is_none = obj is None
is_other_data_type = not isinstance(obj, BaseObject)
if is_none or is_other_data_type:
# Do nothing
return
if obj.is_recorded:
self._block_form()
def _block_form(self):
"""
Overwrites template, providing no actions
Returns:
"""
self._TEMPLATE = "form/recorded_no_edit.html"
class BaseIndexView(BaseView):
""" Base class for index views

View File

@ -26,3 +26,7 @@ class AbstractRecordView(BaseModalFormView):
return ENTRY_RECORDED.format(obj.identifier)
else:
return ENTRY_UNRECORDED.format(obj.identifier)
def _check_for_recorded_instance(self, obj):
# Do not block record view if instance might be recorded
return None

28
konova/views/remove.py Normal file
View File

@ -0,0 +1,28 @@
"""
Author: Michel Peltriaux
Created on: 21.10.25
"""
from django.urls import reverse
from konova.forms.modals import RemoveModalForm
from konova.utils.message_templates import GENERIC_REMOVED_TEMPLATE
from konova.views.base import BaseModalFormView
class BaseRemoveModalFormView(BaseModalFormView):
_MODEL_CLS = None
_FORM_CLS = RemoveModalForm
_MSG_SUCCESS = GENERIC_REMOVED_TEMPLATE
_REDIRECT_URL = None
def _user_has_permission(self, user):
return user.is_default_user()
def _get_redirect_url(self, *args, **kwargs):
return reverse(self._REDIRECT_URL)
def _get_msg_success(self, *args, **kwargs):
obj = kwargs.get("obj", None)
assert obj is not None
return self._MSG_SUCCESS.format(obj.identifier)

View File

@ -22,3 +22,7 @@ class AbstractResubmissionView(LoginRequiredMixin, BaseModalFormView):
def _user_has_permission(self, user):
return user.is_default_user()
def _check_for_recorded_instance(self, obj):
# Resubmissions are allowed despite an entry being recorded
return None

View File

@ -6,24 +6,24 @@ Created on: 22.08.22
"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404, redirect
from django.views import View
from django.utils.translation import gettext_lazy as _
from intervention.forms.modals.share import ShareModalForm
from konova.utils.message_templates import DATA_SHARE_SET
from konova.views.base import BaseView, BaseModalFormView
class AbstractShareByTokenView(View):
model = None
redirect_url = None
class AbstractShareByTokenView(LoginRequiredMixin, BaseView):
_MODEL_CLS = None
_REDIRECT_URL = None
class Meta:
abstract = True
def get(self, request, id: str, token: str):
""" Performs sharing of an intervention
""" Performs sharing of an entry
If token given in url is not valid, the user will be redirected to the dashboard
@ -36,7 +36,7 @@ class AbstractShareByTokenView(View):
"""
user = request.user
obj = get_object_or_404(self.model, id=id)
obj = get_object_or_404(self._MODEL_CLS, id=id)
# Check tokens
if obj.access_token == token:
# Send different messages in case user has already been added to list of sharing users
@ -51,7 +51,7 @@ class AbstractShareByTokenView(View):
_("{} has been shared with you").format(obj.identifier)
)
obj.share_with_user(user)
return redirect(self.redirect_url, id=id)
return redirect(self._REDIRECT_URL, id=id)
else:
messages.error(
request,
@ -60,29 +60,22 @@ class AbstractShareByTokenView(View):
)
return redirect("home")
def _user_has_permission(self, user):
# No permissions are needed to get shared access via token
return True
class AbstractShareFormView(View):
model = None
def _user_has_shared_access(self, user, **kwargs):
# The user does not need to have shared access to call the endpoint which gives them shared access
return True
class AbstractShareFormView(LoginRequiredMixin, BaseModalFormView):
_MODEL_CLS = None
_FORM_CLS = ShareModalForm
_MSG_SUCCESS = DATA_SHARE_SET
class Meta:
abstract = True
def get(self, request, id: str):
""" Renders sharing form
Args:
request (HttpRequest): The incoming request
id (str): Object's id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
form = ShareModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
msg_success=DATA_SHARE_SET
)
def post(self, request, id: str):
return self.get(request, id)
def _user_has_permission(self, user):
return user.is_default_user()

View File

@ -5,103 +5,53 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 22.08.22
"""
from django.shortcuts import get_object_or_404
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse
from django.views import View
from compensation.forms.modals.state import NewCompensationStateModalForm, EditCompensationStateModalForm, \
RemoveCompensationStateModalForm
from compensation.models import CompensationState
from konova.utils.message_templates import COMPENSATION_STATE_ADDED, COMPENSATION_STATE_EDITED, \
COMPENSATION_STATE_REMOVED
from konova.views.base import BaseModalFormView
class AbstractCompensationStateView(View):
model = None
redirect_url = None
class AbstractCompensationStateView(LoginRequiredMixin, BaseModalFormView):
_MODEL_CLS = None
_FORM_CLS = None
_REDIRECT_URL = None
class Meta:
abstract = True
def _user_has_permission(self, user):
return user.is_default_user()
def _get_redirect_url(self, *args, **kwargs):
obj = kwargs.get("obj", None)
assert obj is not None
return reverse(self._REDIRECT_URL, args=(obj.id,)) + "#related_data"
class AbstractNewCompensationStateView(AbstractCompensationStateView):
_MODEL_CLS = None
_FORM_CLS = NewCompensationStateModalForm
_MSG_SUCCESS = COMPENSATION_STATE_ADDED
class Meta:
abstract = True
def get(self, request, id: str):
""" Renders a form for adding new states
Args:
request (HttpRequest): The incoming request
id (str): The object's id to which the new state will be related
Returns:
"""
obj = get_object_or_404(self.model, id=id)
form = NewCompensationStateModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str):
return self.get(request, id)
class AbstractEditCompensationStateView(AbstractCompensationStateView):
_MODEL_CLS = None
_FORM_CLS = EditCompensationStateModalForm
_MSG_SUCCESS = COMPENSATION_STATE_EDITED
class Meta:
abstract = True
def get(self, request, id: str, state_id: str):
""" Renders a form for editing a state
Args:
request (HttpRequest): The incoming request
id (str): The object id
state_id (str): The state's id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = EditCompensationStateModalForm(request.POST or None, instance=obj, state=state, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_EDITED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str, state_id: str):
return self.get(request, id, state_id)
class AbstractRemoveCompensationStateView(AbstractCompensationStateView):
_MODEL_CLS = None
_FORM_CLS = RemoveCompensationStateModalForm
_MSG_SUCCESS = COMPENSATION_STATE_REMOVED
class Meta:
abstract = True
def get(self, request, id: str, state_id: str):
""" Renders a form for removing astate
Args:
request (HttpRequest): The incoming request
id (str): The object id
state_id (str): The state's id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
state = get_object_or_404(CompensationState, id=state_id)
form = RemoveCompensationStateModalForm(request.POST or None, instance=obj, state=state, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_REMOVED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str, state_id: str):
return self.get(request, id, state_id)