Compare commits

..

41 Commits

Author SHA1 Message Date
f146aa983a Merge pull request '# Boost geometry conflict message fetch' (#523) from 503_Improve_performance_on_geometry_conflict_message into master
Reviewed-on: #523
2026-01-14 08:03:01 +00:00
60e9430542 # Boost geometry conflict message fetch
* reduces runtime of geometry conflict info message generating to ~45% to prior runtime
2026-01-14 09:02:41 +01:00
970d0e79fa Merge pull request '490_View_refactoring_II' (#520) from 490_View_refactoring_II into master
Reviewed-on: #520
2026-01-13 09:36:11 +00:00
3f33de3626 # Analysis, API and Payment views
* refactors payment creation, editing and removing into class based views
* refactors analysis report methods into class based views
* drops unused method view on api app (token generating has been de facto moved into users app long time ago)
2026-01-13 10:35:09 +01:00
9e5bb84ab4 Merge pull request '# Compensation sum fix' (#518) from 517_Compensation_sum_wrong into master
Reviewed-on: #518
2026-01-10 10:03:27 +00:00
4c372c1a04 # Compensation sum fix
* fixes sum of compensations on landing page
2026-01-10 11:00:26 +01:00
ee2c859a9e Merge pull request '# Improve exception reporting for API' (#515) from improve_exception_reporting into master
Reviewed-on: #515
2025-12-19 14:17:37 +01:00
328f672ec0 # Improve exception reporting for API
* fixes typo in exception_reporter.py
* properly catches error on geometry cast into multipolygon if input are no valid polygons
* extends error response on malicious api calls
* specifies different exceptions on try-catch while initializing api data
2025-12-19 14:17:15 +01:00
88058d7caf # EcoAccount New and Edit
* refactors new and edit method views into classes
2025-12-17 14:34:04 +01:00
0e6f8d5b55 # Compensation New and Edit
* refactors compensation new and edit method views into classes
2025-12-17 14:24:43 +01:00
047c9489fe Merge pull request '# ExceptionReporter adjustment' (#513) from improve_exception_reporting into master
Reviewed-on: #513
2025-12-17 14:03:23 +01:00
38b81996ed # ExceptionReporter adjustment
* extends the KonovaExceptionReporter to hold POST body content (practical for debugging broken content on API)
2025-12-17 14:02:08 +01:00
3966521cd4 # Revocation Intervention views
* refactors revocation method views for intervention into classes
2025-12-16 16:34:44 +01:00
e70a8b51d1 # Remove-KOM-from-EIV view
* refactors view method into class
2025-12-16 16:25:46 +01:00
02dc0d0a59 # Check view
* refactors method based view into class
2025-12-16 16:21:42 +01:00
0b84d418db # (EMA/EIV) Edit and New view
* refactors 'new' view methods into classes for eiv and ema
* refactors 'edit' view methods into classes for eiv and ema
* reorganizes permissions on non-conservation-office users on ema entries
    * users can now open the log view properly if they have shared access
    * ema actions that require conservation office permission are now hidden on the frontend for non-conservation-office users
2025-12-15 13:02:11 +01:00
6aad76866f # Fixes Permission check order
* fixes bug where permissions would be checked on non-logged in users which caused errors
2025-12-15 09:40:30 +01:00
1af807deae # Remove view
* refactors remove view methods into classes
* introduced AbstractRemoveView
* disables final-delete actions from admin views
* extends error warnings on RemoveEcoAccountModalForm
* removes LoginRequiredMixin from AbstractPublicReportView to make it accessible for the public
* updates translations
2025-12-14 17:37:01 +01:00
a2bda8d230 # QR code
* refactors qr code generating into class
* refactors usage of former qr code method calls
2025-12-14 16:43:31 +01:00
e4c459f92e # Public report
* refactors public report view methods into classes
* introduces AbstractPublicReportView
2025-12-14 16:35:58 +01:00
2da6f1dc6f # Identifier Generator View
* refactors identifier generator view methods into classes
* introduces IdentifierGenerator
* introduces AbstractIdentifierGeneratorView
2025-12-14 16:25:49 +01:00
72914bab9d # Detail View
* refactors detail view methods into classes
* introduces AbstractDetailView
2025-12-14 16:11:50 +01:00
fdf3adf5ae # Index views
* refactors index view methods into classes
* introduces AbstractIndexView as base class
2025-12-14 16:00:40 +01:00
4c4d64cc3d Merge pull request '# HOTFIX: empty geometry save' (#510) from hotfix_empty_geometry_save into master
Reviewed-on: #510
2025-12-03 13:49:55 +01:00
fbde03caec # Optimization
* optimizes logic in case of empty geometry by dropping redundant pre-check on emptiness
2025-12-03 13:48:58 +01:00
43eb598d3f # HOTFIX: empty geometry save
* fixes a bug where the saving of an empty geometry could lead into a json decode error
2025-12-03 13:38:13 +01:00
b7fac0ae03 Merge pull request '# Fix fpr #507' (#508) from 507_Improper_deduction-recording_rendering_on_unrecorded_eco_account into master
Reviewed-on: #508
2025-11-30 12:33:39 +01:00
447ba942b5 # Fix fpr #507
* fixes incorrect rendering of recording-info for deductions on unrecorded eco accounts
2025-11-30 12:32:05 +01:00
6df47f1615 Merge pull request '504_Geometry_read-only_on_editing' (#505) from 504_Geometry_read-only_on_editing into master
Reviewed-on: #505
2025-11-28 11:45:30 +01:00
e25d549a97 # 497 Impressum link update
* updates impressum link
2025-11-28 11:44:18 +01:00
5e65b8f4dc # Geometry error message fix
* fixes bug where errors on geometry form were not rendered properly
* fixes bug where invalid geometry was written as read-only back into form (could not be corrected by user)
* adds explanatory comments to SimpleGeomForm is_valid() checks
* reorders code snippets for better understanding
* adds correcting logic to _set_geojson_properties() in case of missing properties element
2025-11-28 11:43:17 +01:00
22cddb9902 Merge pull request '# Fix for #500' (#501) from 500_Geometry_conflicts_still_visible into master
Reviewed-on: #501
2025-11-19 13:16:30 +01:00
c986bd0b92 # Fix for #500
* fixes bug where de-facto deleted compensations (because of deleted intervention) would still show up as geometry conflicts on other entries
2025-11-19 13:16:09 +01:00
2c60d86177 Merge pull request '# Update netgis map client' (#498) from netgis_map_client into master
Reviewed-on: #498
2025-11-07 14:11:57 +01:00
b7792ececc # Update netgis map client
* updates netgis map client (bugfix for https://github.com/sebastianpauli/netgis-client/issues/43#issuecomment-3446898016)
2025-11-07 14:11:15 +01:00
210f3fcafa Merge pull request '# HOTFIX' (#495) from update_map_config into master
Reviewed-on: #495
2025-10-23 16:12:45 +02:00
e7d67560f2 # HOTFIX
* fixes bug on netgis map client where importing a geometry would result in an error message
* THIS IS JUST A WORKAROUND AND HAS TO BE REPLACED BY A PROPER FIX FROM THE DEVS ASAP
2025-10-23 16:12:05 +02:00
b5e991fb95 Merge pull request '# Map layer update' (#494) from update_map_config into master
Reviewed-on: #494
2025-10-22 16:02:02 +02:00
d3a555d406 # Map layer update
* updates rp_dop layer from deprecated to newest
2025-10-22 16:00:35 +02:00
9a374f50de Merge pull request '# CSS bugfix' (#492) from fix_css_map_menu_bar_overlap into master
Reviewed-on: #492
2025-10-21 15:59:37 +02:00
ce63dd30bc # CSS bugfix
* fixes a rendering bug where the menu bar of the map client overlapped modal forms
2025-10-21 15:57:25 +02:00
134 changed files with 4097 additions and 3011 deletions

View File

@@ -10,6 +10,6 @@ from analysis.views import *
app_name = "analysis" app_name = "analysis"
urlpatterns = [ urlpatterns = [
path("reports/", index_reports_view, name="reports"), path("reports/", ReportIndexView.as_view(), name="reports"),
path("reports/<id>", detail_report_view, name="report-detail"), path("reports/<id>", ReportDetailView.as_view(), name="report-detail"),
] ]

View File

@@ -1,8 +1,12 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import View
from django.views.generic import DetailView
from analysis.forms import TimespanReportForm from analysis.forms import TimespanReportForm
from analysis.utils.excel.excel import TempExcelFile from analysis.utils.excel.excel import TempExcelFile
@@ -42,57 +46,112 @@ def index_reports_view(request: HttpRequest):
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, template, context)
class ReportIndexView(LoginRequiredMixin, View):
@method_decorator(conservation_office_group_required)
def get(self, request: HttpRequest) -> HttpResponse:
@login_required """
@conservation_office_group_required
def detail_report_view(request: HttpRequest, id: str):
""" Renders the detailed report for a conservation office
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The conservation_office KonovaCode id
Returns: Returns:
""" """
# Try to resolve the requested office id template = "analysis/reports/index.html"
cons_office = get_object_or_404( form = TimespanReportForm(None)
KonovaCode, context = {
id=id "form": form
) }
# Try to resolve the date parameters into Date objects -> redirect if this fails context = BaseContext(request, context).context
try: return render(request, template, context)
df = request.GET.get("df", None)
dt = request.GET.get("dt", None) @method_decorator(conservation_office_group_required)
date_from = timezone.make_aware(timezone.datetime.fromisoformat(df)) def post(self, request: HttpRequest) -> HttpResponse:
date_to = timezone.make_aware(timezone.datetime.fromisoformat(dt))
except ValueError: """
messages.error(
request, Args:
PARAMS_INVALID, request (HttpRequest): The incoming request
extra_tags="danger",
Returns:
"""
template = "analysis/reports/index.html"
form = TimespanReportForm(request.POST or None)
if form.is_valid():
redirect_url = form.save()
return redirect(redirect_url)
else:
messages.error(
request,
FORM_INVALID,
extra_tags="danger",
)
context = {
"form": form
}
context = BaseContext(request, context).context
return render(request, template, context)
class ReportDetailView(LoginRequiredMixin, DetailView):
@method_decorator(conservation_office_group_required)
def get(self, request: HttpRequest, id: str):
""" Renders the detailed report for a conservation office
Args:
request (HttpRequest): The incoming request
id (str): The conservation_office KonovaCode id
Returns:
"""
# Try to resolve the requested office id
cons_office = get_object_or_404(
KonovaCode,
id=id
) )
return redirect("analysis:reports") # Try to resolve the date parameters into Date objects -> redirect if this fails
try:
df = request.GET.get("df", None)
dt = request.GET.get("dt", None)
date_from = timezone.make_aware(timezone.datetime.fromisoformat(df))
date_to = timezone.make_aware(timezone.datetime.fromisoformat(dt))
except ValueError:
messages.error(
request,
PARAMS_INVALID,
extra_tags="danger",
)
return redirect("analysis:reports")
# Check whether the html default rendering is requested or an alternative # Check whether the html default rendering is requested or an alternative
format_param = request.GET.get("format", "html") format_param = request.GET.get("format", "html")
report = TimespanReport(id, date_from, date_to) report = TimespanReport(id, date_from, date_to)
if format_param == "html": if format_param == "html":
return self.__handle_html_format(request, report, cons_office)
elif format_param == "excel":
return self.__handle_excel_format(report, cons_office, df, dt)
else:
raise NotImplementedError
def __handle_html_format(self, request, report: TimespanReport, office: KonovaCode):
template = "analysis/reports/detail.html" template = "analysis/reports/detail.html"
context = { context = {
"office": cons_office, "office": office,
"report": report, "report": report,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, template, context)
elif format_param == "excel":
def __handle_excel_format(self, report: TimespanReport, office: KonovaCode, df: str, dt: str):
file = TempExcelFile(report.excel_template_path, report.excel_map) file = TempExcelFile(report.excel_template_path, report.excel_map)
response = HttpResponse( response = HttpResponse(
content=file.stream, content=file.stream,
content_type="application/ms-excel", content_type="application/ms-excel",
) )
response['Content-Disposition'] = f'attachment; filename={cons_office.long_name}_{df}_{dt}.xlsx' response['Content-Disposition'] = f'attachment; filename={office.long_name}_{df}_{dt}.xlsx'
return response return response
else:
raise NotImplementedError

View File

@@ -71,7 +71,7 @@ class APIV1CreateTestCase(BaseAPIV1TestCase):
# Expect this first request to fail, since user has no shared access on the intervention, we want to create # Expect this first request to fail, since user has no shared access on the intervention, we want to create
# a compensation for # a compensation for
response = self._run_create_request(url, post_body) response = self._run_create_request(url, post_body)
self.assertEqual(response.status_code, 500, msg=response.content) self.assertEqual(response.status_code, 400, msg=response.content)
content = json.loads(response.content) content = json.loads(response.content)
self.assertGreater(len(content.get("errors", [])), 0, msg=response.content) self.assertGreater(len(content.get("errors", [])), 0, msg=response.content)

View File

@@ -6,7 +6,9 @@ Created on: 21.01.22
""" """
import json import json
from json import JSONDecodeError
from django.core.exceptions import ObjectDoesNotExist
from django.http import JsonResponse, HttpRequest from django.http import JsonResponse, HttpRequest
from api.utils.serializer.v1.compensation import CompensationAPISerializerV1 from api.utils.serializer.v1.compensation import CompensationAPISerializerV1
@@ -66,8 +68,12 @@ class AbstractAPIViewV1(AbstractAPIView):
body = request.body.decode("utf-8") body = request.body.decode("utf-8")
body = json.loads(body) body = json.loads(body)
created_id = self.serializer.create_model_from_json(body, self.user) created_id = self.serializer.create_model_from_json(body, self.user)
except Exception as e: except (JSONDecodeError,
return self._return_error_response(e, 500) AssertionError,
ValueError,
PermissionError,
ObjectDoesNotExist) as e:
return self._return_error_response(e, 400)
return JsonResponse({"id": created_id}) return JsonResponse({"id": created_id})
def put(self, request: HttpRequest, id=None): def put(self, request: HttpRequest, id=None):

View File

@@ -81,9 +81,7 @@ class AbstractAPIView(View):
Returns: Returns:
""" """
content = [error.__str__()] content = [f"{error.__class__.__name__}: {str(error)}"]
if hasattr(error, "messages"):
content = error.messages
return JsonResponse( return JsonResponse(
{ {
"errors": content "errors": content

0
codelist/views.py Normal file
View File

View File

@@ -45,6 +45,14 @@ class AbstractCompensationAdmin(BaseObjectAdmin):
states = "\n".join(states) states = "\n".join(states)
return states return states
def get_actions(self, request):
DELETE_ACTION_IDENTIFIER = "delete_selected"
actions = super().get_actions(request)
if DELETE_ACTION_IDENTIFIER in actions:
del actions[DELETE_ACTION_IDENTIFIER]
return actions
class CompensationAdmin(AbstractCompensationAdmin): class CompensationAdmin(AbstractCompensationAdmin):
autocomplete_fields = [ autocomplete_fields = [

View File

@@ -168,17 +168,6 @@ class NewCompensationForm(AbstractCompensationForm,
comp.log.add(action) comp.log.add(action)
return comp, action return comp, action
def is_valid(self):
valid = super().is_valid()
intervention = self.cleaned_data.get("intervention", None)
if intervention.is_recorded:
valid &= False
self.add_error(
"intervention",
_("This intervention is currently recorded. You cannot add further compensations as long as it is recorded.")
)
return valid
def save(self, user: User, geom_form: SimpleGeomForm): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
comp, action = self.__create_comp(user) comp, action = self.__create_comp(user)

View File

@@ -15,6 +15,7 @@ from compensation.models import EcoAccount
from intervention.models import Handler, Responsibility, Legal from intervention.models import Handler, Responsibility, Legal
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm from konova.forms.modals import RemoveModalForm
from konova.settings import ETS_GROUP
from konova.utils import validators from konova.utils import validators
from user.models import User, UserActionLogEntry from user.models import User, UserActionLogEntry
@@ -246,4 +247,13 @@ class RemoveEcoAccountModalForm(RemoveModalForm):
"confirm", "confirm",
_("The account can not be removed, since there are still deductions.") _("The account can not be removed, since there are still deductions.")
) )
# If there are deductions but the performing user is not part of an ETS group, we assume this poor
# fella does not know what he/she does -> give a hint that they should contact someone in charge...
user_is_ets_user = self.user.in_group(ETS_GROUP)
if not user_is_ets_user:
self.add_error(
"confirm",
_("Please contact the responsible conservation office to find a solution!")
)
return super_valid and not has_deductions return super_valid and not has_deductions

View File

@@ -7,12 +7,10 @@ Created on: 18.08.22
""" """
from dal import autocomplete from dal import autocomplete
from django import forms from django import forms
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
from compensation.models import CompensationAction
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple
from konova.forms.modals import BaseModalForm, RemoveModalForm from konova.forms.modals import BaseModalForm, RemoveModalForm
from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, ADDED_COMPENSATION_ACTION from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, ADDED_COMPENSATION_ACTION
@@ -116,8 +114,7 @@ class EditCompensationActionModalForm(NewCompensationActionModalForm):
action = None action = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
action_id = kwargs.pop("action_id", None) self.action = kwargs.pop("action", None)
self.action = get_object_or_404(CompensationAction, id=action_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit action") self.form_title = _("Edit action")
form_data = { form_data = {
@@ -150,8 +147,8 @@ class RemoveCompensationActionModalForm(RemoveModalForm):
action = None action = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
action_id = kwargs.pop("action_id", None) action = kwargs.pop("action", None)
self.action = get_object_or_404(CompensationAction, id=action_id) self.action = action
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def save(self): def save(self):

View File

@@ -6,11 +6,10 @@ Created on: 18.08.22
""" """
from django import forms from django import forms
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from konova.forms.modals import BaseModalForm from konova.forms.modals import BaseModalForm
from konova.models import DeadlineType, Deadline from konova.models import DeadlineType
from konova.utils import validators from konova.utils import validators
from konova.utils.message_templates import DEADLINE_EDITED from konova.utils.message_templates import DEADLINE_EDITED
@@ -91,8 +90,7 @@ class EditDeadlineModalForm(NewDeadlineModalForm):
deadline = None deadline = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
deadline_id = kwargs.pop("deadline_id", None) self.deadline = kwargs.pop("deadline", None)
self.deadline = get_object_or_404(Deadline, id=deadline_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit deadline") self.form_title = _("Edit deadline")
form_data = { form_data = {

View File

@@ -6,27 +6,12 @@ Created on: 18.08.22
""" """
from compensation.models import CompensationDocument, EcoAccountDocument from compensation.models import CompensationDocument, EcoAccountDocument
from konova.forms.modals import NewDocumentModalForm, EditDocumentModalForm, RemoveDocumentModalForm from konova.forms.modals import NewDocumentModalForm
class NewCompensationDocumentModalForm(NewDocumentModalForm): class NewCompensationDocumentModalForm(NewDocumentModalForm):
_DOCUMENT_CLS = CompensationDocument document_model = CompensationDocument
class EditCompensationDocumentModalForm(EditDocumentModalForm):
_DOCUMENT_CLS = CompensationDocument
class RemoveCompensationDocumentModalForm(RemoveDocumentModalForm):
_DOCUMENT_CLS = CompensationDocument
class NewEcoAccountDocumentModalForm(NewDocumentModalForm): class NewEcoAccountDocumentModalForm(NewDocumentModalForm):
_DOCUMENT_CLS = EcoAccountDocument document_model = EcoAccountDocument
class EditEcoAccountDocumentModalForm(EditDocumentModalForm):
_DOCUMENT_CLS = EcoAccountDocument
class RemoveEcoAccountDocumentModalForm(RemoveDocumentModalForm):
_DOCUMENT_CLS = EcoAccountDocument

View File

@@ -6,10 +6,8 @@ Created on: 18.08.22
""" """
from django import forms from django import forms
from django.shortcuts import get_object_or_404
from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _ from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _
from compensation.models import Payment
from konova.forms.modals import RemoveModalForm, BaseModalForm from konova.forms.modals import RemoveModalForm, BaseModalForm
from konova.utils import validators from konova.utils import validators
from konova.utils.message_templates import PAYMENT_EDITED from konova.utils.message_templates import PAYMENT_EDITED
@@ -105,8 +103,7 @@ class EditPaymentModalForm(NewPaymentForm):
payment = None payment = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
payment_id = kwargs.pop("payment_id", None) self.payment = kwargs.pop("payment", None)
self.payment = get_object_or_404(Payment, id=payment_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit payment") self.form_title = _("Edit payment")
form_date = { form_date = {
@@ -136,8 +133,8 @@ class RemovePaymentModalForm(RemoveModalForm):
payment = None payment = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
payment_id = kwargs.pop("payment_id", None) payment = kwargs.pop("payment", None)
self.payment = get_object_or_404(Payment, id=payment_id) self.payment = payment
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def save(self): def save(self):

View File

@@ -1,15 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 21.10.25
"""
from compensation.models import Compensation, EcoAccount
from konova.forms.modals import ResubmissionModalForm
class CompensationResubmissionModalForm(ResubmissionModalForm):
_MODEL_CLS = Compensation
class EcoAccountResubmissionModalForm(ResubmissionModalForm):
_MODEL_CLS = EcoAccount

View File

@@ -5,17 +5,21 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22 Created on: 18.08.22
""" """
from bootstrap_modal_forms.mixins import is_ajax
from dal import autocomplete from dal import autocomplete
from django import forms 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 django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, \ from codelist.settings import CODELIST_BIOTOPES_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from compensation.models import CompensationState
from intervention.inputs import CompensationStateTreeRadioSelect from intervention.inputs import CompensationStateTreeRadioSelect
from konova.contexts import BaseContext
from konova.forms.modals import RemoveModalForm, BaseModalForm from konova.forms.modals import RemoveModalForm, BaseModalForm
from konova.utils.message_templates import COMPENSATION_STATE_EDITED, ADDED_COMPENSATION_STATE from konova.utils.message_templates import COMPENSATION_STATE_EDITED, FORM_INVALID, ADDED_COMPENSATION_STATE
class NewCompensationStateModalForm(BaseModalForm): class NewCompensationStateModalForm(BaseModalForm):
@@ -64,13 +68,10 @@ class NewCompensationStateModalForm(BaseModalForm):
) )
) )
_is_before_state: bool = False
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("New state") self.form_title = _("New state")
self.form_caption = _("Insert data for the 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( choices = KonovaCode.objects.filter(
code_lists__in=[CODELIST_BIOTOPES_ID], code_lists__in=[CODELIST_BIOTOPES_ID],
is_archived=False, is_archived=False,
@@ -82,19 +83,65 @@ class NewCompensationStateModalForm(BaseModalForm):
] ]
self.fields["biotope_type"].choices = choices self.fields["biotope_type"].choices = choices
def save(self): def save(self, is_before_state: bool = False):
state = self.instance.add_state(self, self._is_before_state) state = self.instance.add_state(self, is_before_state)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_STATE) self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_STATE)
return 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): class EditCompensationStateModalForm(NewCompensationStateModalForm):
state = None state = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
state_id = kwargs.pop("state_id", None) self.state = kwargs.pop("state", None)
self.state = CompensationState.objects.get(id=state_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit state") self.form_title = _("Edit state")
biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None
@@ -125,8 +172,8 @@ class RemoveCompensationStateModalForm(RemoveModalForm):
state = None state = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
state_id = kwargs.pop("state_id", None) state = kwargs.pop("state", None)
self.state = CompensationState.objects.get(id=state_id) self.state = state
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def save(self): def save(self):

View File

@@ -53,7 +53,7 @@
</td> </td>
<td class="align-middle"> <td class="align-middle">
{% if deduction.intervention.recorded %} {% if deduction.intervention.recorded %}
<em title="{% trans 'Recorded on' %} {{obj.recorded.timestamp}} {% trans 'by' %} {{obj.recorded.user}}" class='fas fa-bookmark registered-bookmark'></em> <em title="{% trans 'Recorded on' %} {{deduction.intervention.recorded.timestamp}} {% trans 'by' %} {{deduction.intervention.recorded.user}}" class='fas fa-bookmark registered-bookmark'></em>
{% else %} {% else %}
<em title="{% trans 'Not recorded yet' %}" class='far fa-bookmark'></em> <em title="{% trans 'Not recorded yet' %}" class='far fa-bookmark'></em>
{% endif %} {% endif %}

View File

@@ -80,11 +80,7 @@ class EditCompensationActionModalFormTestCase(NewCompensationActionModalFormTest
self.compensation.actions.add(self.comp_action) self.compensation.actions.add(self.comp_action)
def test_init(self): def test_init(self):
form = EditCompensationActionModalForm( form = EditCompensationActionModalForm(request=self.request, instance=self.compensation, action=self.comp_action)
request=self.request,
instance=self.compensation,
action_id=self.comp_action.id
)
self.assertEqual(form.form_title, str(_("Edit action"))) self.assertEqual(form.form_title, str(_("Edit action")))
self.assertEqual(len(form.fields["action_type"].initial), self.comp_action.action_type.count()) self.assertEqual(len(form.fields["action_type"].initial), self.comp_action.action_type.count())
self.assertEqual(len(form.fields["action_type_details"].initial), self.comp_action.action_type_details.count()) self.assertEqual(len(form.fields["action_type_details"].initial), self.comp_action.action_type_details.count())
@@ -105,7 +101,7 @@ class EditCompensationActionModalFormTestCase(NewCompensationActionModalFormTest
"comment": comment, "comment": comment,
} }
form = EditCompensationActionModalForm(data, request=self.request, instance=self.compensation, action_id=self.comp_action.id) form = EditCompensationActionModalForm(data, request=self.request, instance=self.compensation, action=self.comp_action)
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
action = form.save() action = form.save()
@@ -130,7 +126,7 @@ class RemoveCompensationActionModalFormTestCase(EditCompensationActionModalFormT
def test_init(self): def test_init(self):
self.assertIn(self.comp_action, self.compensation.actions.all()) self.assertIn(self.comp_action, self.compensation.actions.all())
form = RemoveCompensationActionModalForm(request=self.request, instance=self.compensation, action_id=self.comp_action.id) form = RemoveCompensationActionModalForm(request=self.request, instance=self.compensation, action=self.comp_action)
self.assertEqual(form.action, self.comp_action) self.assertEqual(form.action, self.comp_action)
def test_save(self): def test_save(self):
@@ -141,7 +137,7 @@ class RemoveCompensationActionModalFormTestCase(EditCompensationActionModalFormT
data, data,
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
action_id=self.comp_action.id action=self.comp_action
) )
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
self.assertIn(self.comp_action, self.compensation.actions.all()) self.assertIn(self.comp_action, self.compensation.actions.all())
@@ -190,20 +186,12 @@ class NewCompensationStateModalFormTestCase(BaseTestCase):
self.assertEqual(self.compensation.before_states.count(), 0) self.assertEqual(self.compensation.before_states.count(), 0)
self.assertEqual(self.compensation.after_states.count(), 0) self.assertEqual(self.compensation.after_states.count(), 0)
self.request.GET._mutable = True form = NewCompensationStateModalForm(data, request=self.request, instance=self.compensation)
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) self.assertTrue(form.is_valid(), msg=form.errors)
state = form.save()
is_before_state = True
state = form.save(is_before_state)
self.assertEqual(self.compensation.before_states.count(), 1) self.assertEqual(self.compensation.before_states.count(), 1)
self.assertEqual(self.compensation.after_states.count(), 0) self.assertEqual(self.compensation.after_states.count(), 0)
@@ -217,16 +205,8 @@ class NewCompensationStateModalFormTestCase(BaseTestCase):
self.assertEqual(last_log.action, UserAction.EDITED) self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, ADDED_COMPENSATION_STATE) self.assertEqual(last_log.comment, ADDED_COMPENSATION_STATE)
self.request.GET._mutable = True is_before_state = False
del self.request.GET["before"] state = form.save(is_before_state)
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.before_states.count(), 1)
self.assertEqual(self.compensation.after_states.count(), 1) self.assertEqual(self.compensation.after_states.count(), 1)
@@ -250,11 +230,7 @@ class EditCompensationStateModalFormTestCase(NewCompensationStateModalFormTestCa
self.compensation.after_states.add(self.comp_state) self.compensation.after_states.add(self.comp_state)
def test_init(self): def test_init(self):
form = EditCompensationStateModalForm( form = EditCompensationStateModalForm(request=self.request, instance=self.compensation, state=self.comp_state)
request=self.request,
instance=self.compensation,
state_id=self.comp_state.id
)
self.assertEqual(form.state, self.comp_state) self.assertEqual(form.state, self.comp_state)
self.assertEqual(form.form_title, str(_("Edit state"))) self.assertEqual(form.form_title, str(_("Edit state")))
@@ -285,7 +261,7 @@ class EditCompensationStateModalFormTestCase(NewCompensationStateModalFormTestCa
data, data,
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
state_id=self.comp_state.id state=self.comp_state
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)
@@ -306,11 +282,7 @@ class RemoveCompensationStateModalFormTestCase(EditCompensationStateModalFormTes
super().setUp() super().setUp()
def test_init(self): def test_init(self):
form = RemoveCompensationStateModalForm( form = RemoveCompensationStateModalForm(request=self.request, instance=self.compensation, state=self.comp_state)
request=self.request,
instance=self.compensation,
state_id=self.comp_state.id
)
self.assertEqual(form.state, self.comp_state) self.assertEqual(form.state, self.comp_state)
@@ -322,7 +294,7 @@ class RemoveCompensationStateModalFormTestCase(EditCompensationStateModalFormTes
data, data,
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
state_id=self.comp_state.id state=self.comp_state
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)

View File

@@ -36,7 +36,7 @@ class AbstractCompensationModelTestCase(BaseTestCase):
data, data,
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
deadline_id=self.finished_deadline.id, deadline=self.finished_deadline,
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)
self.assertIn(self.finished_deadline, self.compensation.deadlines.all()) self.assertIn(self.finished_deadline, self.compensation.deadlines.all())

View File

@@ -7,30 +7,31 @@ Created on: 24.08.21
""" """
from django.urls import path from django.urls import path
from compensation.views.compensation.detail import DetailCompensationView
from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \ from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \
GetCompensationDocumentView, RemoveCompensationDocumentView GetCompensationDocumentView, RemoveCompensationDocumentView
from compensation.views.compensation.remove import RemoveCompensationView
from compensation.views.compensation.resubmission import CompensationResubmissionView from compensation.views.compensation.resubmission import CompensationResubmissionView
from compensation.views.compensation.report import CompensationReportView from compensation.views.compensation.report import CompensationPublicReportView
from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \ from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \
RemoveCompensationDeadlineView RemoveCompensationDeadlineView
from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \ from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \
RemoveCompensationActionView RemoveCompensationActionView
from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \ from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \
RemoveCompensationStateView RemoveCompensationStateView
from compensation.views.compensation.compensation import \ from compensation.views.compensation.compensation import IndexCompensationView, CompensationIdentifierGeneratorView, \
CompensationIndexView, CompensationIdentifierGeneratorView, CompensationDetailView, \ EditCompensationView, NewCompensationView
NewCompensationFormView, EditCompensationFormView, RemoveCompensationView
from compensation.views.compensation.log import CompensationLogView from compensation.views.compensation.log import CompensationLogView
urlpatterns = [ urlpatterns = [
# Main compensation # Main compensation
path("", CompensationIndexView.as_view(), name="index"), path("", IndexCompensationView.as_view(), name="index"),
path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'), path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'),
path('new/<intervention_id>', NewCompensationFormView.as_view(), name='new'), path('new/<intervention_id>', NewCompensationView.as_view(), name='new'),
path('new', NewCompensationFormView.as_view(), name='new'), path('new', NewCompensationView.as_view(), name='new'),
path('<id>', CompensationDetailView.as_view(), name='detail'), path('<id>', DetailCompensationView.as_view(), name='detail'),
path('<id>/log', CompensationLogView.as_view(), name='log'), path('<id>/log', CompensationLogView.as_view(), name='log'),
path('<id>/edit', EditCompensationFormView.as_view(), name='edit'), path('<id>/edit', EditCompensationView.as_view(), name='edit'),
path('<id>/remove', RemoveCompensationView.as_view(), name='remove'), path('<id>/remove', RemoveCompensationView.as_view(), name='remove'),
path('<id>/state/new', NewCompensationStateView.as_view(), name='new-state'), path('<id>/state/new', NewCompensationStateView.as_view(), name='new-state'),
@@ -44,7 +45,7 @@ urlpatterns = [
path('<id>/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"), path('<id>/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"),
path('<id>/deadline/<deadline_id>/edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'), path('<id>/deadline/<deadline_id>/edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'), path('<id>/deadline/<deadline_id>/remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'),
path('<id>/report', CompensationReportView.as_view(), name='report'), path('<id>/report', CompensationPublicReportView.as_view(), name='report'),
path('<id>/resub', CompensationResubmissionView.as_view(), name='resubmission-create'), path('<id>/resub', CompensationResubmissionView.as_view(), name='resubmission-create'),
# Documents # Documents

View File

@@ -8,11 +8,13 @@ Created on: 24.08.21
from django.urls import path from django.urls import path
from compensation.autocomplete.eco_account import EcoAccountAutocomplete from compensation.autocomplete.eco_account import EcoAccountAutocomplete
from compensation.views.eco_account.eco_account import EcoAccountIndexView, EcoAccountIdentifierGeneratorView, \ from compensation.views.eco_account.detail import DetailEcoAccountView
EcoAccountDetailView, NewEcoAccountFormView, EditEcoAccountFormView, RemoveEcoAccountView from compensation.views.eco_account.eco_account import IndexEcoAccountView, EcoAccountIdentifierGeneratorView, \
NewEcoAccountView, EditEcoAccountView
from compensation.views.eco_account.log import EcoAccountLogView from compensation.views.eco_account.log import EcoAccountLogView
from compensation.views.eco_account.record import EcoAccountRecordView from compensation.views.eco_account.record import EcoAccountRecordView
from compensation.views.eco_account.report import EcoAccountReportView from compensation.views.eco_account.remove import RemoveEcoAccountView
from compensation.views.eco_account.report import EcoAccountPublicReportView
from compensation.views.eco_account.resubmission import EcoAccountResubmissionView from compensation.views.eco_account.resubmission import EcoAccountResubmissionView
from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \ from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \
RemoveEcoAccountStateView RemoveEcoAccountStateView
@@ -28,14 +30,14 @@ from compensation.views.eco_account.deduction import NewEcoAccountDeductionView,
app_name = "acc" app_name = "acc"
urlpatterns = [ urlpatterns = [
path("", EcoAccountIndexView.as_view(), name="index"), path("", IndexEcoAccountView.as_view(), name="index"),
path('new/', NewEcoAccountFormView.as_view(), name='new'), path('new/', NewEcoAccountView.as_view(), name='new'),
path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'), path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'),
path('<id>', EcoAccountDetailView.as_view(), name='detail'), path('<id>', DetailEcoAccountView.as_view(), name='detail'),
path('<id>/log', EcoAccountLogView.as_view(), name='log'), path('<id>/log', EcoAccountLogView.as_view(), name='log'),
path('<id>/record', EcoAccountRecordView.as_view(), name='record'), path('<id>/record', EcoAccountRecordView.as_view(), name='record'),
path('<id>/report', EcoAccountReportView.as_view(), name='report'), path('<id>/report', EcoAccountPublicReportView.as_view(), name='report'),
path('<id>/edit', EditEcoAccountFormView.as_view(), name='edit'), path('<id>/edit', EditEcoAccountView.as_view(), name='edit'),
path('<id>/remove', RemoveEcoAccountView.as_view(), name='remove'), path('<id>/remove', RemoveEcoAccountView.as_view(), name='remove'),
path('<id>/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'), path('<id>/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'),

View File

@@ -6,7 +6,7 @@ Created on: 24.08.21
""" """
from django.urls import path from django.urls import path
from compensation.views.payment import NewPaymentView, RemovePaymentView, EditPaymentView from compensation.views.payment import *
app_name = "pay" app_name = "pay"
urlpatterns = [ urlpatterns = [

View File

@@ -5,23 +5,53 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.decorators import method_decorator
from compensation.models import Compensation from compensation.forms.modals.compensation_action import RemoveCompensationActionModalForm, \
EditCompensationActionModalForm, NewCompensationActionModalForm
from compensation.models import Compensation, CompensationAction
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_EDITED, \
COMPENSATION_ACTION_ADDED
from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \
AbstractRemoveCompensationActionView AbstractRemoveCompensationActionView
_COMPENSATION_DETAIL_URL_NAME = "compensation:detail"
class NewCompensationActionView(AbstractNewCompensationActionView): class NewCompensationActionView(AbstractNewCompensationActionView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME 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)
class EditCompensationActionView(AbstractEditCompensationActionView): class EditCompensationActionView(AbstractEditCompensationActionView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME 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)
class RemoveCompensationActionView(AbstractRemoveCompensationActionView): class RemoveCompensationActionView(AbstractRemoveCompensationActionView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME 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)

View File

@@ -6,169 +6,260 @@ Created on: 19.08.22
""" """
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ObjectDoesNotExist from django.http import HttpRequest, HttpResponse
from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View
from compensation.forms.compensation import EditCompensationForm, NewCompensationForm from compensation.forms.compensation import EditCompensationForm, NewCompensationForm
from compensation.models import Compensation from compensation.models import Compensation
from compensation.tables.compensation import CompensationTable from compensation.tables.compensation import CompensationTable
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.contexts import BaseContext
from konova.forms.modals import RemoveModalForm from konova.decorators import shared_access_required, default_group_required
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ from konova.forms import SimpleGeomForm
RECORDED_BLOCKS_EDIT, PARAMS_INVALID from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, \
BaseEditSpatialLocatedObjectFormView IDENTIFIER_REPLACED, COMPENSATION_ADDED_TEMPLATE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.detail import BaseDetailView from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.remove import BaseRemoveModalFormView from konova.views.index import AbstractIndexView
class CompensationIndexView(LoginRequiredMixin, BaseIndexView): class IndexCompensationView(AbstractIndexView):
_TAB_TITLE = _("Compensations - Overview") def get(self, request, *args, **kwargs) -> HttpResponse:
_INDEX_TABLE_CLS = CompensationTable """
Renders the index view for compensation
def _get_queryset(self): Args:
qs = Compensation.objects.filter( request (HttpRequest): The incoming request
Returns:
A rendered view
"""
compensations = Compensation.objects.filter(
deleted=None, # only show those which are not deleted individually deleted=None, # only show those which are not deleted individually
intervention__deleted=None, # and don't show the ones whose intervention has been deleted intervention__deleted=None, # and don't show the ones whose intervention has been deleted
).order_by( ).order_by(
"-modified__timestamp" "-modified__timestamp"
) )
return qs table = CompensationTable(
request=request,
queryset=compensations
class NewCompensationFormView(BaseNewSpatialLocatedObjectFormView):
_FORM_CLS = NewCompensationForm
_MODEL_CLS = Compensation
_TEMPLATE = "compensation/form/view.html"
_TAB_TITLE = _("New Compensation")
_REDIRECT_URL = "compensation:detail"
def _user_has_shared_access(self, user, **kwargs):
# On a new compensation make sure the intervention (if call came directly through an intervention's detail
# view) is shared with the user
intervention_id = kwargs.get("intervention_id", None)
if not intervention_id:
return True
else:
intervention = get_object_or_404(Intervention, id=intervention_id)
return intervention.is_shared_with(user)
def _user_has_permission(self, user, **kwargs):
# User has to be an ets user
return user.is_default_user()
def dispatch(self, request, *args, **kwargs):
# Make sure there is an existing intervention based on the given id
# Compensations can not exist without an intervention
intervention_id = kwargs.get("intervention_id", None)
if intervention_id:
try:
intervention = Intervention.objects.get(id=intervention_id)
if intervention.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("intervention:detail", id=intervention_id)
except ObjectDoesNotExist:
messages.error(request, PARAMS_INVALID, extra_tags="danger")
return redirect("home")
return super().dispatch(request, *args, **kwargs)
class EditCompensationFormView(BaseEditSpatialLocatedObjectFormView):
_MODEL_CLS = Compensation
_FORM_CLS = EditCompensationForm
_TEMPLATE = "compensation/form/view.html"
_REDIRECT_URL = "compensation:detail"
def _user_has_permission(self, user, **kwargs):
# User has to be a default user
return user.is_default_user()
class CompensationIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
_MODEL_CLS = Compensation
_REDIRECT_URL = "compensation:index"
class CompensationDetailView(BaseDetailView):
_MODEL_CLS = Compensation
_TEMPLATE = "compensation/detail/compensation/view.html"
def _get_object(self, id: str):
""" Returns the compensation
Args:
id (str): The compensation's id
Returns:
obj (Compensation): The compensation
"""
comp = get_object_or_404(
Compensation.objects.select_related(
"modified",
"created",
"geometry"
),
id=id,
deleted=None,
intervention__deleted=None,
) )
return comp context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
def _get_detail_context(self, obj: Compensation):
""" Generate object specific detail context for view class NewCompensationView(LoginRequiredMixin, View):
_TEMPLATE = "compensation/form/view.html"
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "intervention_id"))
def get(self, request: HttpRequest, intervention_id: str = None, *args, **kwargs) -> HttpResponse:
"""
Renders a view for new compensation
A compensation creation may be called directly from the parent-intervention object. If so - we may take
the intervention's id and directly link the compensation to it.
Args: Args:
obj (): The record request (HttpRequest): The incoming request
intervention_id (str): The intervention identifier
Returns: Returns:
""" """
# Order states according to surface if intervention_id:
before_states = obj.before_states.all().prefetch_related("biotope_type").order_by("-surface") # If the parent-intervention is recorded, we are not allowed to change anything on it's data.
after_states = obj.after_states.all().prefetch_related("biotope_type").order_by("-surface") # Not even adding new child elements like compensations!
actions = obj.actions.all().prefetch_related("action_type") intervention = get_object_or_404(Intervention, id=intervention_id)
recording_state_blocks_actions = intervention.is_recorded
if recording_state_blocks_actions:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("intervention:detail", id=intervention_id)
# Precalculate logical errors between before- and after-states data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling geom_form = SimpleGeomForm(request.POST or None, read_only=False)
sum_before_states = obj.get_surface_before_states()
sum_after_states = obj.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
last_checked = obj.intervention.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
last_checked.get_timestamp_str_formatted(),
last_checked.user
)
context = { context = {
"last_checked": last_checked, "form": data_form,
"last_checked_tooltip": last_checked_tooltip, "geom_form": geom_form,
"actions": actions, TAB_TITLE_IDENTIFIER: _("New compensation"),
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"has_finished_deadlines": obj.get_finished_deadlines().exists(),
} }
return context context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "intervention_id"))
def post(self, request: HttpRequest, intervention_id: str = None, *args, **kwargs) -> HttpResponse:
"""
Renders a view for a new compensation creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
if intervention_id:
# If the parent-intervention is recorded, we are not allowed to change anything on it's data.
# Not even adding new child elements like compensations!
intervention = get_object_or_404(Intervention, id=intervention_id)
recording_state_blocks_actions = intervention.is_recorded
if recording_state_blocks_actions:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("intervention:detail", id=intervention_id)
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
comp = data_form.save(request.user, geom_form)
if generated_identifier != comp.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
comp.identifier
)
)
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("compensation:detail", id=comp.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger", )
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New compensation"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
class RemoveCompensationView(LoginRequiredMixin, BaseRemoveModalFormView): class CompensationIdentifierGeneratorView(AbstractIdentifierGeneratorView):
_MODEL_CLS = Compensation _MODEL = Compensation
_FORM_CLS = RemoveModalForm
_REDIRECT_URL = "compensation:index"
def _user_has_permission(self, user, **kwargs):
return user.is_default_user() class EditCompensationView(LoginRequiredMixin, View):
_TEMPLATE = "compensation/form/view.html"
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
# Get object from db
comp = get_object_or_404(Compensation, id=id)
if comp.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditCompensationForm(request.POST or None, instance=comp)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
# Get object from db
comp = get_object_or_404(Compensation, id=id)
if comp.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditCompensationForm(request.POST or None, instance=comp)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
if data_form.is_valid() and geom_form.is_valid():
# Preserve state of intervention checked to determine whether the user must be informed or not
# about a change of the check state
intervention_is_checked = comp.intervention.checked is not None
# The data form takes the geom form for processing, as well as the performing user
comp = data_form.save(request.user, geom_form)
if intervention_is_checked:
messages.info(request, CHECK_STATE_RESET)
messages.success(request, _("Compensation {} edited").format(comp.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("compensation:detail", id=comp.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger", )
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)

View File

@@ -5,21 +5,45 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 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 compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.deadline import AbstractRemoveDeadlineView, AbstractEditDeadlineView, AbstractNewDeadlineView from konova.views.deadline import AbstractRemoveDeadlineView, AbstractEditDeadlineView, AbstractNewDeadlineView
_COMPENSATION_DETAIL_URL_NAME = "compensation:detail"
class NewCompensationDeadlineView(AbstractNewDeadlineView): class NewCompensationDeadlineView(AbstractNewDeadlineView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME 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)
class EditCompensationDeadlineView(AbstractEditDeadlineView): class EditCompensationDeadlineView(AbstractEditDeadlineView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME 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)
class RemoveCompensationDeadlineView(AbstractRemoveDeadlineView): class RemoveCompensationDeadlineView(AbstractRemoveDeadlineView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME 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)

View File

@@ -0,0 +1,97 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from django.contrib import messages
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, get_object_or_404
from compensation.models import Compensation
from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.settings import ETS_GROUP, ZB_GROUP, DEFAULT_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE, DATA_CHECKED_PREVIOUSLY_TEMPLATE
from konova.views.detail import AbstractDetailView
class DetailCompensationView(AbstractDetailView):
_TEMPLATE = "compensation/detail/compensation/view.html"
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" Renders a detail view for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
comp = get_object_or_404(
Compensation.objects.select_related(
"modified",
"created",
"geometry"
),
id=id,
deleted=None,
intervention__deleted=None,
)
geom_form = SimpleGeomForm(instance=comp)
parcels = comp.get_underlying_parcels()
_user = request.user
is_data_shared = comp.intervention.is_shared_with(_user)
# Order states according to surface
before_states = comp.before_states.all().prefetch_related("biotope_type").order_by("-surface")
after_states = comp.after_states.all().prefetch_related("biotope_type").order_by("-surface")
actions = comp.actions.all().prefetch_related("action_type")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = comp.get_surface_before_states()
sum_after_states = comp.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
request = comp.set_status_messages(request)
last_checked = comp.intervention.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
last_checked.get_timestamp_str_formatted(),
last_checked.user
)
requesting_user_is_only_shared_user = comp.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = {
"obj": comp,
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"geom_form": geom_form,
"parcels": parcels,
"is_entry_shared": is_data_shared,
"actions": actions,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": comp.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}",
"has_finished_deadlines": comp.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)

View File

@@ -5,33 +5,62 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from compensation.forms.modals.document import NewCompensationDocumentModalForm, EditCompensationDocumentModalForm, \ from django.contrib.auth.decorators import login_required
RemoveCompensationDocumentModalForm from django.utils.decorators import method_decorator
from compensation.forms.modals.document import NewCompensationDocumentModalForm
from compensation.models import Compensation, CompensationDocument from compensation.models import Compensation, CompensationDocument
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.forms.modals import EditDocumentModalForm
from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \
AbstractEditDocumentView AbstractEditDocumentView
class NewCompensationDocumentView(AbstractNewDocumentView): class NewCompensationDocumentView(AbstractNewDocumentView):
_MODEL_CLS = Compensation model = Compensation
_FORM_CLS = NewCompensationDocumentModalForm form = NewCompensationDocumentModalForm
_REDIRECT_URL = "compensation:detail" 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)
class GetCompensationDocumentView(AbstractGetDocumentView): class GetCompensationDocumentView(AbstractGetDocumentView):
_MODEL_CLS = Compensation model = Compensation
_DOCUMENT_CLS = CompensationDocument document_model = CompensationDocument
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveCompensationDocumentView(AbstractRemoveDocumentView): class RemoveCompensationDocumentView(AbstractRemoveDocumentView):
_MODEL_CLS = Compensation model = Compensation
_DOCUMENT_CLS = CompensationDocument document_model = CompensationDocument
_FORM_CLS = RemoveCompensationDocumentModalForm
_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)
class EditCompensationDocumentView(AbstractEditDocumentView): class EditCompensationDocumentView(AbstractEditDocumentView):
_MODEL_CLS = Compensation model = Compensation
_DOCUMENT_CLS = CompensationDocument document_model = CompensationDocument
_FORM_CLS = EditCompensationDocumentModalForm form = EditDocumentModalForm
_REDIRECT_URL = "compensation:detail" 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)

View File

@@ -5,11 +5,20 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import Compensation from compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.log import AbstractLogView from konova.views.log import AbstractLogView
class CompensationLogView(LoginRequiredMixin, AbstractLogView): class CompensationLogView(AbstractLogView):
_MODEL_CLS = Compensation model = Compensation
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,20 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from django.http import HttpRequest, HttpResponse
from django.utils.decorators import method_decorator
from compensation.models import Compensation
from konova.decorators import shared_access_required
from konova.views.remove import AbstractRemoveView
class RemoveCompensationView(AbstractRemoveView):
_MODEL = Compensation
_REDIRECT_URL = "compensation:index"
@method_decorator(shared_access_required(Compensation, "id"))
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
return super().get(request, *args, **kwargs)

View File

@@ -5,48 +5,81 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.models import Compensation from compensation.models import Compensation
from konova.sub_settings.django_settings import BASE_URL from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.qrcode import QrCode from konova.utils.qrcode import QrCode
from konova.views.report import BaseReportView from konova.views.report import AbstractPublicReportView
class BaseCompensationReportView(BaseReportView): class CompensationPublicReportView(AbstractPublicReportView):
def _get_compensation_report_context(self, obj):
# Order states by surface
before_states = obj.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = obj.after_states.all().order_by("-surface").prefetch_related("biotope_type")
actions = obj.actions.all().prefetch_related("action_type")
return {
"before_states": before_states,
"after_states": after_states,
"actions": actions,
}
class CompensationReportView(BaseCompensationReportView):
_MODEL = Compensation
_TEMPLATE = "compensation/report/compensation/report.html" _TEMPLATE = "compensation/report/compensation/report.html"
def _get_report_context(self, obj): def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
report_url = BASE_URL + reverse("compensation:report", args=(obj.id,)) """ Renders the public report view
qrcode_report = QrCode(report_url, 10)
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
report_context = { Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
tab_title = _("Report {}").format(comp.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not comp.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=comp
)
parcels = comp.get_underlying_parcels()
qrcode = QrCode(
content=request.build_absolute_uri(reverse("compensation:report", args=(id,))),
size=10
)
qrcode_lanis = QrCode(
content=comp.get_LANIS_link(),
size=7
)
# Order states by surface
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type")
actions = comp.actions.all().prefetch_related("action_type")
context = {
"obj": comp,
"qrcode": { "qrcode": {
"img": qrcode_report.get_img(), "img": qrcode.get_img(),
"url": qrcode_report.get_content(), "url": qrcode.get_content(),
}, },
"qrcode_lanis": { "qrcode_lanis": {
"img": qrcode_lanis.get_img(), "img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(), "url": qrcode_lanis.get_content(),
}, },
"is_entry_shared": False, # disables action buttons during rendering "is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"tables_scrollable": False, "tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title,
} }
report_context.update(self._get_compensation_report_context(obj)) context = BaseContext(request, context).context
return report_context return render(request, self._TEMPLATE, context)

View File

@@ -5,12 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from compensation.forms.modals.resubmission import CompensationResubmissionModalForm from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import Compensation from compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.resubmission import AbstractResubmissionView from konova.views.resubmission import AbstractResubmissionView
class CompensationResubmissionView(AbstractResubmissionView): class CompensationResubmissionView(AbstractResubmissionView):
_MODEL_CLS = Compensation model = Compensation
_FORM_CLS = CompensationResubmissionModalForm redirect_url_base = "compensation:detail"
_REDIRECT_URL = "compensation:detail" form_action_url_base = "compensation:resubmission-create"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,21 +5,46 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 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 compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \
AbstractRemoveCompensationStateView AbstractRemoveCompensationStateView
class NewCompensationStateView(AbstractNewCompensationStateView): class NewCompensationStateView(AbstractNewCompensationStateView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = "compensation:detail" 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)
class EditCompensationStateView(AbstractEditCompensationStateView): class EditCompensationStateView(AbstractEditCompensationStateView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = "compensation:detail" 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)
class RemoveCompensationStateView(AbstractRemoveCompensationStateView): class RemoveCompensationStateView(AbstractRemoveCompensationStateView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = "compensation:detail" 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)

View File

@@ -5,22 +5,46 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 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 compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \
AbstractRemoveCompensationActionView AbstractRemoveCompensationActionView
_ECO_ACCOUNT_DETAIL_URL_NAME = "compensation:acc:detail"
class NewEcoAccountActionView(AbstractNewCompensationActionView): class NewEcoAccountActionView(AbstractNewCompensationActionView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME 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)
class EditEcoAccountActionView(AbstractEditCompensationActionView): class EditEcoAccountActionView(AbstractEditCompensationActionView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME 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)
class RemoveEcoAccountActionView(AbstractRemoveCompensationActionView): class RemoveEcoAccountActionView(AbstractRemoveCompensationActionView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME 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)

View File

@@ -5,22 +5,45 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 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 compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.deadline import AbstractNewDeadlineView, AbstractEditDeadlineView, AbstractRemoveDeadlineView from konova.views.deadline import AbstractNewDeadlineView, AbstractEditDeadlineView, AbstractRemoveDeadlineView
_ECO_ACCOUNT_DETAIL_URL_NAME = "compensation:acc:detail"
class NewEcoAccountDeadlineView(AbstractNewDeadlineView): class NewEcoAccountDeadlineView(AbstractNewDeadlineView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME 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)
class EditEcoAccountDeadlineView(AbstractEditDeadlineView): class EditEcoAccountDeadlineView(AbstractEditDeadlineView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME 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)
class RemoveEcoAccountDeadlineView(AbstractRemoveDeadlineView): class RemoveEcoAccountDeadlineView(AbstractRemoveDeadlineView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME 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)

View File

@@ -5,33 +5,54 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.http import Http404 from django.http import Http404
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.decorators import default_group_required, login_required_modal
from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView
_ECO_ACCOUNT_DETAIl_URL_NAME = "compensation:acc:detail"
class NewEcoAccountDeductionView(LoginRequiredMixin, AbstractNewDeductionView): class NewEcoAccountDeductionView(AbstractNewDeductionView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _custom_check(self, obj): def _custom_check(self, obj):
# New deductions can only be created if the eco account has been recorded
if not obj.recorded: if not obj.recorded:
raise Http404() raise Http404()
def _check_for_recorded_instance(self, obj):
# Deductions can be created on recorded as well as on non-recorded entries class EditEcoAccountDeductionView(AbstractEditDeductionView):
return None def _custom_check(self, obj):
pass
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditEcoAccountDeductionView(LoginRequiredMixin, AbstractEditDeductionView): class RemoveEcoAccountDeductionView(AbstractRemoveDeductionView):
_MODEL_CLS = EcoAccount def _custom_check(self, obj):
_REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME pass
model = EcoAccount
redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEcoAccountDeductionView(LoginRequiredMixin, AbstractRemoveDeductionView):
_MODEL_CLS = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME

View File

@@ -0,0 +1,97 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from django.contrib import messages
from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, get_object_or_404
from compensation.models import EcoAccount
from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.settings import ETS_GROUP, ZB_GROUP, DEFAULT_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE
from konova.views.detail import AbstractDetailView
class DetailEcoAccountView(AbstractDetailView):
_TEMPLATE = "compensation/detail/eco_account/view.html"
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" Renders a detail view for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
acc = get_object_or_404(
EcoAccount.objects.prefetch_related(
"deadlines",
).select_related(
'geometry',
'responsible',
),
id=id,
deleted=None,
)
geom_form = SimpleGeomForm(instance=acc)
parcels = acc.get_underlying_parcels()
_user = request.user
is_data_shared = acc.is_shared_with(_user)
# Order states according to surface
before_states = acc.before_states.order_by("-surface")
after_states = acc.after_states.order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = acc.get_surface_before_states()
sum_after_states = acc.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
# Calculate rest of available surface for deductions
available_total = acc.deductable_rest
available_relative = acc.get_deductable_rest_relative()
# Prefetch related data to decrease the amount of db connections
deductions = acc.deductions.filter(
intervention__deleted=None,
)
actions = acc.actions.all()
request = acc.set_status_messages(request)
requesting_user_is_only_shared_user = acc.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = {
"obj": acc,
"geom_form": geom_form,
"parcels": parcels,
"is_entry_shared": is_data_shared,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"available": available_relative,
"available_total": available_total,
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": acc.get_LANIS_link(),
"deductions": deductions,
"actions": actions,
TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}",
"has_finished_deadlines": acc.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)

View File

@@ -5,33 +5,65 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from compensation.forms.modals.document import NewEcoAccountDocumentModalForm, RemoveEcoAccountDocumentModalForm, \ from django.contrib.auth.decorators import login_required
EditEcoAccountDocumentModalForm from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.decorators import method_decorator
from compensation.forms.modals.document import NewEcoAccountDocumentModalForm
from compensation.models import EcoAccount, EcoAccountDocument from compensation.models import EcoAccount, EcoAccountDocument
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.forms.modals import EditDocumentModalForm
from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \
AbstractEditDocumentView AbstractEditDocumentView
class NewEcoAccountDocumentView(AbstractNewDocumentView): class NewEcoAccountDocumentView(AbstractNewDocumentView):
_MODEL_CLS = EcoAccount model = EcoAccount
_FORM_CLS = NewEcoAccountDocumentModalForm form = NewEcoAccountDocumentModalForm
_REDIRECT_URL = "compensation:acc:detail" 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)
class GetEcoAccountDocumentView(AbstractGetDocumentView): class GetEcoAccountDocumentView(AbstractGetDocumentView):
_MODEL_CLS = EcoAccount model = EcoAccount
_DOCUMENT_CLS = EcoAccountDocument document_model = EcoAccountDocument
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEcoAccountDocumentView(AbstractRemoveDocumentView): class RemoveEcoAccountDocumentView(AbstractRemoveDocumentView):
_MODEL_CLS = EcoAccount model = EcoAccount
_DOCUMENT_CLS = EcoAccountDocument document_model = EcoAccountDocument
_FORM_CLS = RemoveEcoAccountDocumentModalForm
_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)
class EditEcoAccountDocumentView(AbstractEditDocumentView): class EditEcoAccountDocumentView(AbstractEditDocumentView):
_MODEL_CLS = EcoAccount model = EcoAccount
_DOCUMENT_CLS = EcoAccountDocument document_model = EcoAccountDocument
_FORM_CLS = EditEcoAccountDocumentModalForm form = EditDocumentModalForm
_REDIRECT_URL = "compensation:acc:detail" 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)

View File

@@ -6,81 +6,94 @@ Created on: 19.08.22
""" """
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View
from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm, RemoveEcoAccountModalForm from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm
from compensation.models import EcoAccount from compensation.models import EcoAccount
from compensation.tables.eco_account import EcoAccountTable from compensation.tables.eco_account import EcoAccountTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required, login_required_modal from konova.decorators import shared_access_required, default_group_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.settings import ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER 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, \ from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, FORM_INVALID, \
IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ from konova.views.identifier import AbstractIdentifierGeneratorView
BaseEditSpatialLocatedObjectFormView from konova.views.index import AbstractIndexView
from konova.views.detail import BaseDetailView
from konova.views.remove import BaseRemoveModalFormView
class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView): class IndexEcoAccountView(AbstractIndexView):
_INDEX_TABLE_CLS = EcoAccountTable def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
_TAB_TITLE = _("Eco-account - Overview") """
Renders the index view for eco accounts
def _get_queryset(self): Args:
qs = EcoAccount.objects.filter( request (HttpRequest): The incoming request
Returns:
A rendered view
"""
eco_accounts = EcoAccount.objects.filter(
deleted=None, deleted=None,
).order_by( ).order_by(
"-modified__timestamp" "-modified__timestamp"
) )
return qs table = EcoAccountTable(
request=request,
queryset=eco_accounts
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
class NewEcoAccountFormView(BaseNewSpatialLocatedObjectFormView): class NewEcoAccountView(LoginRequiredMixin, View):
_FORM_CLS = NewEcoAccountForm
_MODEL_CLS = EcoAccount
_TEMPLATE = "compensation/form/view.html" _TEMPLATE = "compensation/form/view.html"
_TAB_TITLE = _("New Eco-Account")
_REDIRECT_URL = "compensation:acc:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(default_group_required)
# User has to be a default user def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
return user.is_default_user() """
Renders a view for a new eco account creation
Args:
request (HttpRequest): The incoming request
class EditEcoAccountFormView(BaseEditSpatialLocatedObjectFormView): Returns:
_FORM_CLS = EditEcoAccountForm
_MODEL_CLS = EcoAccount
_TEMPLATE = "compensation/form/view.html"
_REDIRECT_URL = "compensation:acc:detail"
def _user_has_permission(self, user, **kwargs): """
# User has to be a default user data_form = NewEcoAccountForm(request.POST or None)
return user.is_default_user() geom_form = SimpleGeomForm(request.POST or None, read_only=False)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New Eco-Account"),
}
context = BaseContext(request, context).context
@login_required return render(request, self._TEMPLATE, context)
@default_group_required
def new_view(request: HttpRequest):
"""
Renders a view for a new eco account creation
Args: @method_decorator(default_group_required)
request (HttpRequest): The incoming request def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
Returns: """
Renders a view for a new eco account creation
""" Args:
template = "compensation/form/view.html" request (HttpRequest): The incoming request
data_form = NewEcoAccountForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False) Returns:
if request.method == "POST":
"""
data_form = NewEcoAccountForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if data_form.is_valid() and geom_form.is_valid(): if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None) generated_identifier = data_form.cleaned_data.get("identifier", None)
acc = data_form.save(request.user, geom_form) acc = data_form.save(request.user, geom_form)
@@ -98,61 +111,92 @@ def new_view(request: HttpRequest):
request, request,
GEOMETRY_SIMPLIFIED GEOMETRY_SIMPLIFIED
) )
num_ignored_geometries = geom_form.get_num_geometries_ignored() num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0: if num_ignored_geometries > 0:
messages.info( messages.info(
request, request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
) )
return redirect("compensation:acc:detail", id=acc.id) return redirect("compensation:acc:detail", id=acc.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger", )
else:
# For clarification: nothing in this case context = {
pass "form": data_form,
context = { "geom_form": geom_form,
"form": data_form, TAB_TITLE_IDENTIFIER: _("New Eco-Account"),
"geom_form": geom_form, }
TAB_TITLE_IDENTIFIER: _("New Eco-Account"), context = BaseContext(request, context).context
}
context = BaseContext(request, context).context return render(request, self._TEMPLATE, context)
return render(request, template, context)
class EcoAccountIdentifierGeneratorView(AbstractIdentifierGeneratorView):
_MODEL = EcoAccount
class EcoAccountIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView): class EditEcoAccountView(LoginRequiredMixin, View):
_MODEL_CLS = EcoAccount _TEMPLATE = "compensation/form/view.html"
_REDIRECT_URL = "compensation:acc:index"
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
@login_required """
@default_group_required Renders a view for editing compensations
@shared_access_required(EcoAccount, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
Returns: Returns:
""" """
template = "compensation/form/view.html" # Get object from db
# Get object from db acc = get_object_or_404(EcoAccount, id=id)
acc = get_object_or_404(EcoAccount, id=id) if acc.is_recorded:
if acc.is_recorded: messages.info(
messages.info( request,
request, RECORDED_BLOCKS_EDIT
RECORDED_BLOCKS_EDIT )
) return redirect("compensation:acc:detail", id=id)
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)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
# 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)
# 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() data_form_valid = data_form.is_valid()
geom_form_valid = geom_form.is_valid() geom_form_valid = geom_form.is_valid()
if data_form_valid and geom_form_valid: if data_form_valid and geom_form_valid:
@@ -164,101 +208,21 @@ def edit_view(request: HttpRequest, id: str):
request, request,
GEOMETRY_SIMPLIFIED GEOMETRY_SIMPLIFIED
) )
num_ignored_geometries = geom_form.get_num_geometries_ignored() num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0: if num_ignored_geometries > 0:
messages.info( messages.info(
request, request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
) )
return redirect("compensation:acc:detail", id=acc.id) return redirect("compensation:acc:detail", id=acc.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) 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):
_MODEL_CLS = EcoAccount
_TEMPLATE = "compensation/detail/eco_account/view.html"
def _get_object(self, id: str):
""" Fetch object for detail view
Args:
id (str): The record's id'
Returns:
"""
acc = get_object_or_404(
EcoAccount.objects.prefetch_related(
"deadlines",
).select_related(
'geometry',
'responsible',
),
id=id,
deleted=None,
)
return acc
def _get_detail_context(self, obj: EcoAccount):
""" Generate object specific detail context for view
Args:
obj (): The record
Returns:
"""
# Order states according to surface
before_states = obj.before_states.order_by("-surface")
after_states = obj.after_states.order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = obj.get_surface_before_states()
sum_after_states = obj.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
# Calculate rest of available surface for deductions
available_total = obj.deductable_rest
available_relative = obj.get_deductable_rest_relative()
# Prefetch related data to decrease the amount of db connections
deductions = obj.deductions.filter(
intervention__deleted=None,
)
actions = obj.actions.all()
context = { context = {
"before_states": before_states, "form": data_form,
"after_states": after_states, "geom_form": geom_form,
"sum_before_states": sum_before_states, TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier),
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"available": available_relative,
"available_total": available_total,
"deductions": deductions,
"actions": actions,
"has_finished_deadlines": obj.get_finished_deadlines().exists(),
} }
return context context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
class RemoveEcoAccountView(LoginRequiredMixin, BaseRemoveModalFormView):
_MODEL_CLS = EcoAccount
_FORM_CLS = RemoveEcoAccountModalForm
_REDIRECT_URL = "compensation:acc:index"
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()

View File

@@ -5,11 +5,20 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.log import AbstractLogView from konova.views.log import AbstractLogView
class EcoAccountLogView(LoginRequiredMixin, AbstractLogView): class EcoAccountLogView(AbstractLogView):
_MODEL_CLS = EcoAccount 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)

View File

@@ -5,12 +5,20 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.record import AbstractRecordView from konova.views.record import AbstractRecordView
class EcoAccountRecordView(LoginRequiredMixin, AbstractRecordView): class EcoAccountRecordView(AbstractRecordView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,22 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from django.http import HttpRequest, HttpResponse
from django.utils.decorators import method_decorator
from compensation.forms.eco_account import RemoveEcoAccountModalForm
from compensation.models import EcoAccount
from konova.decorators import shared_access_required
from konova.views.remove import AbstractRemoveView
class RemoveEcoAccountView(AbstractRemoveView):
_MODEL = EcoAccount
_REDIRECT_URL = "compensation:acc:index"
_FORM = RemoveEcoAccountModalForm
@method_decorator(shared_access_required(EcoAccount, "id"))
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
return super().get(request, *args, **kwargs)

View File

@@ -5,41 +5,88 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount from compensation.models import EcoAccount
from compensation.views.compensation.report import BaseCompensationReportView from konova.contexts import BaseContext
from konova.sub_settings.django_settings import BASE_URL from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.qrcode import QrCode from konova.utils.qrcode import QrCode
from konova.views.report import AbstractPublicReportView
class EcoAccountReportView(BaseCompensationReportView): class EcoAccountPublicReportView(AbstractPublicReportView):
_MODEL = EcoAccount
_TEMPLATE = "compensation/report/eco_account/report.html" _TEMPLATE = "compensation/report/eco_account/report.html"
def _get_report_context(self, obj): def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
report_url = BASE_URL + reverse("compensation:acc:report", args=(obj.id,)) """ Renders the public report view
qrcode_report = QrCode(report_url, 10)
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7) Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
tab_title = _("Report {}").format(acc.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not acc.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=acc
)
parcels = acc.get_underlying_parcels()
qrcode = QrCode(
content=request.build_absolute_uri(reverse("compensation:acc:report", args=(id,))),
size=10
)
qrcode_lanis = QrCode(
content=acc.get_LANIS_link(),
size=7
)
# Order states by surface
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent")
after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent")
actions = acc.actions.all().prefetch_related("action_type__parent")
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier) # Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier)
deductions = obj.deductions.all() \ deductions = acc.deductions.all() \
.distinct("intervention") \ .distinct("intervention") \
.select_related("intervention") \ .select_related("intervention") \
.values_list("intervention__id", "intervention__identifier", "intervention__title", named=True) .values_list("intervention__id", "intervention__identifier", "intervention__title", named=True)
report_context = { context = {
"obj": acc,
"qrcode": { "qrcode": {
"img": qrcode_report.get_img(), "img": qrcode.get_img(),
"url": qrcode_report.get_content(), "url": qrcode.get_content(),
}, },
"qrcode_lanis": { "qrcode_lanis": {
"img": qrcode_lanis.get_img(), "img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(), "url": qrcode_lanis.get_content(),
}, },
"is_entry_shared": False, # disables action buttons during rendering "is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"deductions": deductions, "deductions": deductions,
"tables_scrollable": False, "tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title,
} }
report_context.update(self._get_compensation_report_context(obj)) context = BaseContext(request, context).context
return report_context return render(request, self._TEMPLATE, context)

View File

@@ -5,12 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from compensation.forms.modals.resubmission import EcoAccountResubmissionModalForm from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.resubmission import AbstractResubmissionView from konova.views.resubmission import AbstractResubmissionView
class EcoAccountResubmissionView(AbstractResubmissionView): class EcoAccountResubmissionView(AbstractResubmissionView):
_MODEL_CLS = EcoAccount model = EcoAccount
_FORM_CLS = EcoAccountResubmissionModalForm redirect_url_base = "compensation:acc:detail"
_REDIRECT_URL = "compensation:acc:detail" form_action_url_base = "compensation:acc:resubmission-create"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

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

View File

@@ -5,21 +5,46 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 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 compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \
AbstractRemoveCompensationStateView AbstractRemoveCompensationStateView
class NewEcoAccountStateView(AbstractNewCompensationStateView): class NewEcoAccountStateView(AbstractNewCompensationStateView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = "compensation:acc:detail" 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)
class EditEcoAccountStateView(AbstractEditCompensationStateView): class EditEcoAccountStateView(AbstractEditCompensationStateView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = "compensation:acc:detail" 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)
class RemoveEcoAccountStateView(AbstractRemoveCompensationStateView): class RemoveEcoAccountStateView(AbstractRemoveCompensationStateView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = "compensation:acc:detail" 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)

View File

@@ -6,37 +6,110 @@ Created on: 09.08.21
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views import View
from compensation.forms.modals.payment import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm from compensation.forms.modals.payment import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm
from compensation.models import Payment
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import default_group_required, shared_access_required
from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED
from konova.views.base import BaseModalFormView
class BasePaymentView(LoginRequiredMixin, BaseModalFormView): class NewPaymentView(LoginRequiredMixin, View):
_MODEL_CLS = Intervention
_REDIRECT_URL = "intervention:detail"
class Meta: def __process_request(self, request: HttpRequest, id: str):
abstract = True """ Renders a modal view for adding new payments
def _get_redirect_url(self, *args, **kwargs): Args:
url = super()._get_redirect_url(*args, **kwargs) request (HttpRequest): The incoming request
return f"{url}#related_data" id (str): The intervention's id for which a new payment shall be added
def _user_has_permission(self, user, **kwargs): Returns:
return user.is_default_user()
"""
intervention = get_object_or_404(Intervention, id=id)
form = NewPaymentForm(request.POST or None, instance=intervention, request=request)
return form.process_request(
request,
msg_success=PAYMENT_ADDED,
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str):
return self.__process_request(request, id=id)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str):
return self.__process_request(request, id=id)
class NewPaymentView(BasePaymentView): class RemovePaymentView(LoginRequiredMixin, View):
_FORM_CLS = NewPaymentForm
_MSG_SUCCESS = PAYMENT_ADDED def __process_request(self, request: HttpRequest, id: str, payment_id: str):
""" Renders a modal view for removing payments
Args:
request (HttpRequest): The incoming request
id (str): The intervention's id
payment_id (str): The payment's id
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
payment = get_object_or_404(Payment, id=payment_id)
form = RemovePaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request)
return form.process_request(
request=request,
msg_success=PAYMENT_REMOVED,
redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data"
)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, payment_id: str):
return self.__process_request(request, id=id, payment_id=payment_id)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str, payment_id: str):
return self.__process_request(request, id=id, payment_id=payment_id)
class EditPaymentView(BasePaymentView): class EditPaymentView(LoginRequiredMixin, View):
_MSG_SUCCESS = PAYMENT_EDITED def __process_request(self, request: HttpRequest, id: str, payment_id: str):
_FORM_CLS = EditPaymentModalForm """ Renders a modal view for editing payments
class RemovePaymentView(BasePaymentView): Args:
_MSG_SUCCESS = PAYMENT_REMOVED request (HttpRequest): The incoming request
_FORM_CLS = RemovePaymentModalForm id (str): The intervention's id
payment_id (str): The payment's id
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
payment = get_object_or_404(Payment, id=payment_id)
form = EditPaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request)
return form.process_request(
request=request,
msg_success=PAYMENT_EDITED,
redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data"
)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, payment_id: str):
return self.__process_request(request, id=id, payment_id=payment_id)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str, payment_id: str):
return self.__process_request(request, id=id, payment_id=payment_id)

View File

@@ -15,8 +15,7 @@ from compensation.forms.compensation import AbstractCompensationForm
from ema.models import Ema, EmaDocument from ema.models import Ema, EmaDocument
from intervention.models import Responsibility, Handler from intervention.models import Responsibility, Handler
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import NewDocumentModalForm, EditDocumentModalForm, RemoveDocumentModalForm, \ from konova.forms.modals import NewDocumentModalForm
ResubmissionModalForm
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -171,13 +170,4 @@ class EditEmaForm(NewEmaForm):
class NewEmaDocumentModalForm(NewDocumentModalForm): class NewEmaDocumentModalForm(NewDocumentModalForm):
_DOCUMENT_CLS = EmaDocument document_model = EmaDocument
class EditEmaDocumentModalForm(EditDocumentModalForm):
_DOCUMENT_CLS = EmaDocument
class RemoveEmaDocumentModalForm(RemoveDocumentModalForm):
_DOCUMENT_CLS = EmaDocument
class EmaResubmissionModalForm(ResubmissionModalForm):
_MODEL_CLS = Ema

View File

@@ -15,10 +15,10 @@
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}"> <button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}">
{% fa5_icon 'bell' %} {% fa5_icon 'bell' %}
</button> </button>
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-form' obj.id %}">
{% fa5_icon 'share-alt' %}
</button>
{% if is_ets_member %} {% if is_ets_member %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-form' obj.id %}">
{% fa5_icon 'share-alt' %}
</button>
{% if obj.recorded %} {% if obj.recorded %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'ema:record' obj.id %}"> <button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'ema:record' obj.id %}">
{% fa5_icon 'bookmark' 'far' %} {% fa5_icon 'bookmark' 'far' %}
@@ -28,19 +28,21 @@
{% fa5_icon 'bookmark' %} {% fa5_icon 'bookmark' %}
</button> </button>
{% endif %} {% endif %}
<a href="{% url 'ema:edit' obj.id %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}">
{% fa5_icon 'edit' %}
</button>
</a>
{% endif %} {% endif %}
{% if is_default_member %} {% if is_default_member %}
<a href="{% url 'ema:edit' obj.id %}" class="mr-2"> <button class="btn btn-default btn-modal mr-2" data-form-url="{% url 'ema:log' obj.id %}" title="{% trans 'Show log' %}">
<button class="btn btn-default" title="{% trans 'Edit' %}"> {% fa5_icon 'history' %}
{% fa5_icon 'edit' %} </button>
{% endif %}
{% if is_ets_member %}
<button class="btn btn-default btn-modal" data-form-url="{% url 'ema:remove' obj.id %}" title="{% trans 'Delete' %}">
{% fa5_icon 'trash' %}
</button> </button>
</a>
<button class="btn btn-default btn-modal mr-2" data-form-url="{% url 'ema:log' obj.id %}" title="{% trans 'Show log' %}">
{% fa5_icon 'history' %}
</button>
<button class="btn btn-default btn-modal" data-form-url="{% url 'ema:remove' obj.id %}" title="{% trans 'Delete' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>

View File

@@ -118,6 +118,7 @@ class EmaViewTestCase(CompensationViewTestCase):
self.index_url, self.index_url,
self.detail_url, self.detail_url,
self.report_url, self.report_url,
self.log_url,
] ]
fail_urls = [ fail_urls = [
self.new_url, self.new_url,
@@ -133,7 +134,6 @@ class EmaViewTestCase(CompensationViewTestCase):
self.action_remove_url, self.action_remove_url,
self.action_new_url, self.action_new_url,
self.new_doc_url, self.new_doc_url,
self.log_url,
self.remove_url, self.remove_url,
] ]
self.assert_url_fail(client, fail_urls) self.assert_url_fail(client, fail_urls)

View File

@@ -9,27 +9,28 @@ from django.urls import path
from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView
from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView
from ema.views.detail import DetailEmaView
from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView
from ema.views.ema import EmaIndexView, EmaIdentifierGeneratorView, EmaDetailView, EditEmaFormView, NewEmaFormView, \ from ema.views.ema import IndexEmaView, EmaIdentifierGeneratorView, EditEmaView, NewEmaView
RemoveEmaView
from ema.views.log import EmaLogView from ema.views.log import EmaLogView
from ema.views.record import EmaRecordView from ema.views.record import EmaRecordView
from ema.views.report import EmaReportView from ema.views.remove import RemoveEmaView
from ema.views.report import EmaPublicReportView
from ema.views.resubmission import EmaResubmissionView from ema.views.resubmission import EmaResubmissionView
from ema.views.share import EmaShareFormView, EmaShareByTokenView from ema.views.share import EmaShareFormView, EmaShareByTokenView
from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView
app_name = "ema" app_name = "ema"
urlpatterns = [ urlpatterns = [
path("", EmaIndexView.as_view(), name="index"), path("", IndexEmaView.as_view(), name="index"),
path("new/", NewEmaFormView.as_view(), name="new"), path("new/", NewEmaView.as_view(), name="new"),
path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"), path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"),
path("<id>", EmaDetailView.as_view(), name="detail"), path("<id>", DetailEmaView.as_view(), name="detail"),
path('<id>/log', EmaLogView.as_view(), name='log'), path('<id>/log', EmaLogView.as_view(), name='log'),
path('<id>/edit', EditEmaFormView.as_view(), name='edit'), path('<id>/edit', EditEmaView.as_view(), name='edit'),
path('<id>/remove', RemoveEmaView.as_view(), name='remove'), path('<id>/remove', RemoveEmaView.as_view(), name='remove'),
path('<id>/record', EmaRecordView.as_view(), name='record'), path('<id>/record', EmaRecordView.as_view(), name='record'),
path('<id>/report', EmaReportView.as_view(), name='report'), path('<id>/report', EmaPublicReportView.as_view(), name='report'),
path('<id>/resub', EmaResubmissionView.as_view(), name='resubmission-create'), path('<id>/resub', EmaResubmissionView.as_view(), name='resubmission-create'),
path('<id>/state/new', NewEmaStateView.as_view(), name='new-state'), path('<id>/state/new', NewEmaStateView.as_view(), name='new-state'),

View File

@@ -5,31 +5,46 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 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 ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \
AbstractRemoveCompensationActionView AbstractRemoveCompensationActionView
_EMA_ACCOUNT_DETAIL_URL_NAME = "ema:detail"
class NewEmaActionView(AbstractNewCompensationActionView): class NewEmaActionView(AbstractNewCompensationActionView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME redirect_url = "ema:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
class EditEmaActionView(AbstractEditCompensationActionView): class EditEmaActionView(AbstractEditCompensationActionView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME redirect_url = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEmaActionView(AbstractRemoveCompensationActionView): class RemoveEmaActionView(AbstractRemoveCompensationActionView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME redirect_url = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @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)

View File

@@ -5,30 +5,46 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 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 ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.deadline import AbstractNewDeadlineView, AbstractRemoveDeadlineView, AbstractEditDeadlineView from konova.views.deadline import AbstractNewDeadlineView, AbstractRemoveDeadlineView, AbstractEditDeadlineView
_EMA_DETAIL_URL_NAME = "ema:detail"
class NewEmaDeadlineView(AbstractNewDeadlineView): class NewEmaDeadlineView(AbstractNewDeadlineView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = _EMA_DETAIL_URL_NAME redirect_url = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditEmaDeadlineView(AbstractEditDeadlineView): class EditEmaDeadlineView(AbstractEditDeadlineView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = _EMA_DETAIL_URL_NAME redirect_url = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEmaDeadlineView(AbstractRemoveDeadlineView): class RemoveEmaDeadlineView(AbstractRemoveDeadlineView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = _EMA_DETAIL_URL_NAME redirect_url = "ema:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()

76
ema/views/detail.py Normal file
View File

@@ -0,0 +1,76 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from django.contrib import messages
from django.http import HttpResponse, HttpRequest
from django.shortcuts import get_object_or_404, render
from ema.models import Ema
from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE
from konova.views.detail import AbstractDetailView
class DetailEmaView(AbstractDetailView):
_TEMPLATE = "ema/detail/view.html"
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" Renders the detail view of an EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA id
Returns:
"""
ema = get_object_or_404(Ema, id=id, deleted=None)
geom_form = SimpleGeomForm(instance=ema)
parcels = ema.get_underlying_parcels()
_user = request.user
is_entry_shared = ema.is_shared_with(_user)
# Order states according to surface
before_states = ema.before_states.all().order_by("-surface")
after_states = ema.after_states.all().order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = ema.get_surface_before_states()
sum_after_states = ema.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
ema.set_status_messages(request)
requesting_user_is_only_shared_user = ema.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = {
"obj": ema,
"geom_form": geom_form,
"parcels": parcels,
"is_entry_shared": is_entry_shared,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": ema.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}",
"has_finished_deadlines": ema.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)

View File

@@ -5,41 +5,62 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from ema.forms import NewEmaDocumentModalForm, RemoveEmaDocumentModalForm, EditEmaDocumentModalForm from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.forms import NewEmaDocumentModalForm
from ema.models import Ema, EmaDocument from ema.models import Ema, EmaDocument
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.forms.modals import EditDocumentModalForm
from konova.views.document import AbstractEditDocumentView, AbstractRemoveDocumentView, AbstractGetDocumentView, \ from konova.views.document import AbstractEditDocumentView, AbstractRemoveDocumentView, AbstractGetDocumentView, \
AbstractNewDocumentView AbstractNewDocumentView
class NewEmaDocumentView(AbstractNewDocumentView): class NewEmaDocumentView(AbstractNewDocumentView):
_MODEL_CLS = Ema model = Ema
_FORM_CLS = NewEmaDocumentModalForm form = NewEmaDocumentModalForm
_REDIRECT_URL = "ema:detail" redirect_url = "ema:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
class GetEmaDocumentView(AbstractGetDocumentView): class GetEmaDocumentView(AbstractGetDocumentView):
_MODEL_CLS = Ema model = Ema
_DOCUMENT_CLS = EmaDocument document_model = EmaDocument
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
class RemoveEmaDocumentView(AbstractRemoveDocumentView): class RemoveEmaDocumentView(AbstractRemoveDocumentView):
_MODEL_CLS = Ema model = Ema
_DOCUMENT_CLS = EmaDocument document_model = EmaDocument
_FORM_CLS = RemoveEmaDocumentModalForm
_REDIRECT_URL = "ema:detail" @method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
class EditEmaDocumentView(AbstractEditDocumentView): class EditEmaDocumentView(AbstractEditDocumentView):
_MODEL_CLS = Ema model = Ema
_FORM_CLS = EditEmaDocumentModalForm document_model = EmaDocument
_DOCUMENT_CLS = EmaDocument form = EditDocumentModalForm
_REDIRECT_URL = "ema:detail" redirect_url = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @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)

View File

@@ -5,112 +5,228 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 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.contrib.auth.mixins import LoginRequiredMixin
from django.shortcuts import get_object_or_404 from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic.base import View
from ema.forms import NewEmaForm, EditEmaForm from ema.forms import NewEmaForm, EditEmaForm
from ema.models import Ema from ema.models import Ema
from ema.tables import EmaTable from ema.tables import EmaTable
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ from konova.contexts import BaseContext
BaseEditSpatialLocatedObjectFormView from konova.decorators import shared_access_required, conservation_office_group_required
from konova.views.detail import BaseDetailView from konova.forms import SimpleGeomForm
from konova.views.remove import BaseRemoveModalFormView from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.index import AbstractIndexView
class EmaIndexView(LoginRequiredMixin, BaseIndexView): class IndexEmaView(AbstractIndexView):
_TAB_TITLE = _("EMAs - Overview") def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
_INDEX_TABLE_CLS = EmaTable """ Renders the index view for EMAs
def _get_queryset(self): Args:
qs = Ema.objects.filter( request (HttpRequest): The incoming request
Returns:
"""
emas = Ema.objects.filter(
deleted=None, deleted=None,
).order_by( ).order_by(
"-modified__timestamp" "-modified__timestamp"
) )
return qs
class NewEmaFormView(BaseNewSpatialLocatedObjectFormView):
_FORM_CLS = NewEmaForm
_MODEL_CLS = Ema
_TEMPLATE = "ema/form/view.html"
_TAB_TITLE = _("New EMA")
_REDIRECT_URL = "ema:detail"
def _user_has_permission(self, user, **kwargs):
# User has to be an ets user
return user.is_ets_user()
class EditEmaFormView(BaseEditSpatialLocatedObjectFormView):
_MODEL_CLS = Ema
_FORM_CLS = EditEmaForm
_TEMPLATE = "ema/form/view.html"
_REDIRECT_URL = "ema:detail"
_TAB_TITLE = _("Edit {}")
def _user_has_permission(self, user, **kwargs):
# User has to be an ets user
return user.is_ets_user()
class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:index"
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
class EmaDetailView(BaseDetailView):
_MODEL_CLS = Ema
_TEMPLATE = "ema/detail/view.html"
def _get_object(self, id: str):
""" Fetch object for detail view
Args:
id (str): The record's id'
Returns:
"""
ema = get_object_or_404(Ema, id=id, deleted=None)
return ema
def _get_detail_context(self, obj: Ema):
""" Generate object specific detail context for view
Args:
obj (): The record
Returns:
"""
# Order states according to surface
before_states = obj.before_states.all().order_by("-surface")
after_states = obj.after_states.all().order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = obj.get_surface_before_states()
sum_after_states = obj.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
table = EmaTable(
request,
queryset=emas
)
context = { context = {
"before_states": before_states, "table": table,
"after_states": after_states, TAB_TITLE_IDENTIFIER: _("EMAs - Overview"),
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"has_finished_deadlines": obj.get_finished_deadlines().exists(),
} }
return context context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
class RemoveEmaView(LoginRequiredMixin, BaseRemoveModalFormView): class NewEmaView(LoginRequiredMixin, View):
_MODEL_CLS = Ema _TEMPLATE = "ema/form/view.html"
_REDIRECT_URL = "ema:index"
def _user_has_permission(self, user, **kwargs): @method_decorator(conservation_office_group_required)
return user.is_ets_user() def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
""" GET endpoint
Renders form for new EMA
Args:
request (HttpRequest): The incoming request
*args ():
**kwargs ():
Returns:
"""
data_form = NewEmaForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New EMA"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(conservation_office_group_required)
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
""" POST endpoint
Processes submitted form
Args:
request (HttpRequest): The incoming request
*args ():
**kwargs ():
Returns:
"""
data_form = NewEmaForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
ema = data_form.save(request.user, geom_form)
if generated_identifier != ema.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
ema.identifier
)
)
messages.success(request, _("EMA {} added").format(ema.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("ema:detail", id=ema.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New EMA"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
class EmaIdentifierGeneratorView(AbstractIdentifierGeneratorView):
_MODEL = Ema
@method_decorator(conservation_office_group_required)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
return super().get(request, *args, **kwargs)
class EditEmaView(LoginRequiredMixin, View):
_TEMPLATE = "compensation/form/view.html"
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" GET endpoint
Renders form
Args:
request (HttpRequest): The incoming request
id (str): The ema identifier
*args ():
**kwargs ():
Returns:
"""
# Get object from db
ema = get_object_or_404(Ema, id=id)
if ema.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("ema:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEmaForm(instance=ema)
geom_form = SimpleGeomForm(read_only=False, instance=ema)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def post(self, request: HttpRequest, id:str, *args, **kwargs) -> HttpResponse:
""" POST endpoint
Process submitted forms
Args:
request (HttpRequest): The incoming request
id (str): The id of the ema
*args ():
**kwargs ():
Returns:
"""
# Get object from db
ema = get_object_or_404(Ema, id=id)
if ema.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("ema:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEmaForm(request.POST or None, instance=ema)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema)
if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user
ema = data_form.save(request.user, geom_form)
messages.success(request, _("EMA {} edited").format(ema.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("ema:detail", id=ema.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger", )
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)

View File

@@ -5,14 +5,19 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema from ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.log import AbstractLogView from konova.views.log import AbstractLogView
class EmaLogView(LoginRequiredMixin, AbstractLogView): class EmaLogView(AbstractLogView):
_MODEL_CLS = Ema model = Ema
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @method_decorator(login_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,12 +5,20 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema from ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.record import AbstractRecordView from konova.views.record import AbstractRecordView
class EmaRecordView(LoginRequiredMixin, AbstractRecordView): class EmaRecordView(AbstractRecordView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = "ema:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

21
ema/views/remove.py Normal file
View File

@@ -0,0 +1,21 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from django.http import HttpRequest, HttpResponse
from django.utils.decorators import method_decorator
from ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required
from konova.views.remove import AbstractRemoveView
class RemoveEmaView(AbstractRemoveView):
_MODEL = Ema
_REDIRECT_URL = "ema:index"
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
return super().get(request, *args, **kwargs)

View File

@@ -5,36 +5,81 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.views.compensation.report import BaseCompensationReportView
from ema.models import Ema from ema.models import Ema
from konova.sub_settings.django_settings import BASE_URL from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.qrcode import QrCode from konova.utils.qrcode import QrCode
from konova.views.report import AbstractPublicReportView
class EmaReportView(BaseCompensationReportView): class EmaPublicReportView(AbstractPublicReportView):
_TEMPLATE = "ema/report/report.html" _TEMPLATE = "ema/report/report.html"
_MODEL = Ema
def _get_report_context(self, obj): def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
report_url = BASE_URL + reverse("ema:report", args=(obj.id,)) """ Renders the public report view
qrcode_report = QrCode(report_url, 10)
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
generic_compensation_report_context = self._get_compensation_report_context(obj) Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
report_context = { Returns:
"""
ema = get_object_or_404(Ema, id=id)
tab_title = _("Report {}").format(ema.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not ema.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=ema,
)
parcels = ema.get_underlying_parcels()
qrcode = QrCode(
content=request.build_absolute_uri(reverse("ema:report", args=(id,))),
size=10
)
qrcode_lanis = QrCode(
content=ema.get_LANIS_link(),
size=7
)
# Order states by surface
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type")
actions = ema.actions.all().prefetch_related("action_type")
context = {
"obj": ema,
"qrcode": { "qrcode": {
"img": qrcode_report.get_img(), "img": qrcode.get_img(),
"url": qrcode_report.get_content(), "url": qrcode.get_content(),
}, },
"qrcode_lanis": { "qrcode_lanis": {
"img": qrcode_lanis.get_img(), "img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(), "url": qrcode_lanis.get_content(),
}, },
"is_entry_shared": False, # disables action buttons during rendering "is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"tables_scrollable": False, "tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title,
} }
report_context.update(generic_compensation_report_context) context = BaseContext(request, context).context
return report_context return render(request, self._TEMPLATE, context)

View File

@@ -5,16 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from ema.forms import EmaResubmissionModalForm from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema from ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.resubmission import AbstractResubmissionView from konova.views.resubmission import AbstractResubmissionView
class EmaResubmissionView(AbstractResubmissionView): class EmaResubmissionView(AbstractResubmissionView):
_MODEL_CLS = Ema model = Ema
_FORM_CLS = EmaResubmissionModalForm redirect_url_base = "ema:detail"
_REDIRECT_URL = "ema:detail" form_action_url_base = "ema:resubmission-create"
action_url = "ema:resubmission-create"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @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)

View File

@@ -5,17 +5,29 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 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 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 from konova.views.share import AbstractShareByTokenView, AbstractShareFormView
class EmaShareByTokenView(AbstractShareByTokenView): class EmaShareByTokenView(AbstractShareByTokenView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = "ema:detail" redirect_url = "ema:detail"
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EmaShareFormView(AbstractShareFormView): class EmaShareFormView(AbstractShareFormView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @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)

View File

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

View File

@@ -37,6 +37,14 @@ class InterventionAdmin(BaseObjectAdmin):
"geometry", "geometry",
] ]
def get_actions(self, request):
DELETE_ACTION_IDENTIFIER = "delete_selected"
actions = super().get_actions(request)
if DELETE_ACTION_IDENTIFIER in actions:
del actions[DELETE_ACTION_IDENTIFIER]
return actions
class InterventionDocumentAdmin(AbstractDocumentAdmin): class InterventionDocumentAdmin(AbstractDocumentAdmin):
pass pass

View File

@@ -172,8 +172,7 @@ class EditEcoAccountDeductionModalForm(NewEcoAccountDeductionModalForm):
deduction = None deduction = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
deduction_id = kwargs.pop("deduction_id", None) self.deduction = kwargs.pop("deduction", None)
self.deduction = EcoAccountDeduction.objects.get(id=deduction_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit Deduction") self.form_title = _("Edit Deduction")
form_data = { form_data = {
@@ -253,20 +252,19 @@ class RemoveEcoAccountDeductionModalForm(RemoveModalForm):
Can be used for anything, where removing shall be confirmed by the user a second time. Can be used for anything, where removing shall be confirmed by the user a second time.
""" """
_DEDUCTION_OBJ = None deduction = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
deduction_id = kwargs.pop("deduction_id", None) deduction = kwargs.pop("deduction", None)
deduction = EcoAccountDeduction.objects.get(id=deduction_id) self.deduction = deduction
self._DEDUCTION_OBJ = deduction
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def save(self): def save(self):
with transaction.atomic(): with transaction.atomic():
self._DEDUCTION_OBJ.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) self.deduction.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED)
self._DEDUCTION_OBJ.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) self.deduction.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED)
self._DEDUCTION_OBJ.delete() self.deduction.delete()
def check_for_recorded_instance(self): def check_for_recorded_instance(self):
if self._DEDUCTION_OBJ.intervention.is_recorded: if self.deduction.intervention.is_recorded:
self.block_form() self.block_form()

View File

@@ -6,11 +6,11 @@ Created on: 18.08.22
""" """
from intervention.models import InterventionDocument from intervention.models import InterventionDocument
from konova.forms.modals import NewDocumentModalForm, EditDocumentModalForm, RemoveDocumentModalForm from konova.forms.modals import NewDocumentModalForm
class NewInterventionDocumentModalForm(NewDocumentModalForm): class NewInterventionDocumentModalForm(NewDocumentModalForm):
_DOCUMENT_CLS = InterventionDocument document_model = InterventionDocument
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" Extension of regular NewDocumentModalForm """ Extension of regular NewDocumentModalForm
@@ -28,31 +28,3 @@ class NewInterventionDocumentModalForm(NewDocumentModalForm):
self.instance.send_data_to_egon() self.instance.send_data_to_egon()
return doc return doc
class EditInterventionDocumentModalForm(EditDocumentModalForm):
_DOCUMENT_CLS = InterventionDocument
def save(self, *args, **kwargs):
""" Extension of regular EditDocumentModalForm
Checks whether payments exist on the intervention and sends the data to EGON
Args:
*args ():
**kwargs ():
Returns:
"""
doc = super().save(*args, **kwargs)
self.instance.send_data_to_egon()
return doc
class RemoveInterventionDocumentModalForm(RemoveDocumentModalForm):
_DOCUMENT_CLS = InterventionDocument
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.instance.send_data_to_egon()

View File

@@ -1,22 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 08.11.25
"""
from django.shortcuts import get_object_or_404
from compensation.models import Compensation
from konova.forms.modals import RemoveModalForm
class RemoveCompensationFromInterventionModalForm(RemoveModalForm):
""" Specific form for removing a compensation from an intervention
"""
def __init__(self, *args, **kwargs):
# The 'instance' that is pushed into the constructor by the generic base class points to the
# intervention instead of the compensation, which shall be deleted. Therefore we need to fetch the compensation
# and replace the instance parameter with that object
instance = get_object_or_404(Compensation, id=kwargs.pop("comp_id"))
kwargs["instance"] = instance
super().__init__(*args, **kwargs)

View File

@@ -1,11 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 21.10.25
"""
from intervention.models import Intervention
from konova.forms.modals import ResubmissionModalForm
class InterventionResubmissionModalForm(ResubmissionModalForm):
_MODEL_CLS = Intervention

View File

@@ -7,10 +7,9 @@ Created on: 18.08.22
""" """
from django import forms from django import forms
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from intervention.models import RevocationDocument, Revocation from intervention.models import RevocationDocument
from konova.forms.modals import BaseModalForm, RemoveModalForm from konova.forms.modals import BaseModalForm, RemoveModalForm
from konova.utils import validators from konova.utils import validators
from konova.utils.message_templates import REVOCATION_ADDED, REVOCATION_EDITED from konova.utils.message_templates import REVOCATION_ADDED, REVOCATION_EDITED
@@ -76,8 +75,7 @@ class EditRevocationModalForm(NewRevocationModalForm):
revocation = None revocation = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
revocation_id = kwargs.pop("revocation_id", None) self.revocation = kwargs.pop("revocation", None)
self.revocation = get_object_or_404(Revocation, id=revocation_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit revocation") self.form_title = _("Edit revocation")
try: try:
@@ -106,8 +104,8 @@ class RemoveRevocationModalForm(RemoveModalForm):
revocation = None revocation = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
revocation_id = kwargs.pop("revocation_id", None) revocation = kwargs.pop("revocation", None)
self.revocation = get_object_or_404(Revocation, id=revocation_id) self.revocation = revocation
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def save(self): def save(self):

View File

@@ -280,7 +280,7 @@ class EditRevocationModalFormTestCase(NewRevocationModalFormTestCase):
data, data,
request=self.request, request=self.request,
instance=self.intervention, instance=self.intervention,
revocation_id=self.revoc.id revocation=self.revoc
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)
obj = form.save() obj = form.save()
@@ -302,7 +302,7 @@ class RemoveRevocationModalFormTestCase(EditRevocationModalFormTestCase):
form = RemoveRevocationModalForm( form = RemoveRevocationModalForm(
request=self.request, request=self.request,
instance=self.intervention, instance=self.intervention,
revocation_id=self.revoc.id, revocation=self.revoc,
) )
self.assertEqual(form.instance, self.intervention) self.assertEqual(form.instance, self.intervention)
self.assertEqual(form.revocation, self.revoc) self.assertEqual(form.revocation, self.revoc)
@@ -317,7 +317,7 @@ class RemoveRevocationModalFormTestCase(EditRevocationModalFormTestCase):
data, data,
request=self.request, request=self.request,
instance=self.intervention, instance=self.intervention,
revocation_id=self.revoc.id revocation=self.revoc
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)
form.save() form.save()

View File

@@ -14,30 +14,32 @@ from intervention.views.deduction import NewInterventionDeductionView, EditInter
RemoveInterventionDeductionView RemoveInterventionDeductionView
from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \ from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \
RemoveInterventionDocumentView, EditInterventionDocumentView RemoveInterventionDocumentView, EditInterventionDocumentView
from intervention.views.intervention import InterventionIndexView, InterventionIdentifierGeneratorView, \ from intervention.views.intervention import IndexInterventionView, InterventionIdentifierGeneratorView, \
InterventionDetailView, NewInterventionFormView, EditInterventionFormView, RemoveInterventionView NewInterventionView, EditInterventionView
from intervention.views.remove import RemoveInterventionView
from intervention.views.detail import DetailInterventionView
from intervention.views.log import InterventionLogView from intervention.views.log import InterventionLogView
from intervention.views.record import InterventionRecordView from intervention.views.record import InterventionRecordView
from intervention.views.report import InterventionReportView from intervention.views.report import InterventionPublicReportView
from intervention.views.resubmission import InterventionResubmissionView from intervention.views.resubmission import InterventionResubmissionView
from intervention.views.revocation import NewRevocationView, GetRevocationDocumentView, EditRevocationView, \ from intervention.views.revocation import NewInterventionRevocationView, GetInterventionRevocationView, \
RemoveRevocationView EditInterventionRevocationView, RemoveInterventionRevocationView
from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView
app_name = "intervention" app_name = "intervention"
urlpatterns = [ urlpatterns = [
path("", InterventionIndexView.as_view(), name="index"), path("", IndexInterventionView.as_view(), name="index"),
path('new/', NewInterventionFormView.as_view(), name='new'), path('new/', NewInterventionView.as_view(), name='new'),
path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'), path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'),
path('<id>', InterventionDetailView.as_view(), name='detail'), path('<id>', DetailInterventionView.as_view(), name='detail'),
path('<id>/log', InterventionLogView.as_view(), name='log'), path('<id>/log', InterventionLogView.as_view(), name='log'),
path('<id>/edit', EditInterventionFormView.as_view(), name='edit'), path('<id>/edit', EditInterventionView.as_view(), name='edit'),
path('<id>/remove', RemoveInterventionView.as_view(), name='remove'), path('<id>/remove', RemoveInterventionView.as_view(), name='remove'),
path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'), path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'),
path('<id>/share', InterventionShareFormView.as_view(), name='share-form'), path('<id>/share', InterventionShareFormView.as_view(), name='share-form'),
path('<id>/check', InterventionCheckView.as_view(), name='check'), path('<id>/check', InterventionCheckView.as_view(), name='check'),
path('<id>/record', InterventionRecordView.as_view(), name='record'), path('<id>/record', InterventionRecordView.as_view(), name='record'),
path('<id>/report', InterventionReportView.as_view(), name='report'), path('<id>/report', InterventionPublicReportView.as_view(), name='report'),
path('<id>/resub', InterventionResubmissionView.as_view(), name='resubmission-create'), path('<id>/resub', InterventionResubmissionView.as_view(), name='resubmission-create'),
# Compensations # Compensations
@@ -55,10 +57,10 @@ urlpatterns = [
path('<id>/deduction/<deduction_id>/remove', RemoveInterventionDeductionView.as_view(), name='remove-deduction'), path('<id>/deduction/<deduction_id>/remove', RemoveInterventionDeductionView.as_view(), name='remove-deduction'),
# Revocation routes # Revocation routes
path('<id>/revocation/new', NewRevocationView.as_view(), name='new-revocation'), path('<id>/revocation/new', NewInterventionRevocationView.as_view(), name='new-revocation'),
path('<id>/revocation/<revocation_id>/edit', EditRevocationView.as_view(), name='edit-revocation'), path('<id>/revocation/<revocation_id>/edit', EditInterventionRevocationView.as_view(), name='edit-revocation'),
path('<id>/revocation/<revocation_id>/remove', RemoveRevocationView.as_view(), name='remove-revocation'), path('<id>/revocation/<revocation_id>/remove', RemoveInterventionRevocationView.as_view(), name='remove-revocation'),
path('revocation/<doc_id>', GetRevocationDocumentView.as_view(), name='get-doc-revocation'), path('revocation/<doc_id>', GetInterventionRevocationView.as_view(), name='get-doc-revocation'),
# Autocomplete # Autocomplete
path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="autocomplete"), path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="autocomplete"),

View File

@@ -6,23 +6,43 @@ Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View
from intervention.forms.modals.check import CheckModalForm from intervention.forms.modals.check import CheckModalForm
from intervention.models import Intervention from intervention.models import Intervention
from konova.views.base import BaseModalFormView from konova.decorators import registration_office_group_required, shared_access_required
from konova.utils.message_templates import INTERVENTION_INVALID
class InterventionCheckView(LoginRequiredMixin, View):
class InterventionCheckView(LoginRequiredMixin, BaseModalFormView): def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
_MODEL_CLS = Intervention """ Renders check form for an intervention
_FORM_CLS = CheckModalForm
_MSG_SUCCESS = _("Check performed")
_REDIRECT_URL = "intervention:detail"
def _user_has_permission(self, user, **kwargs): Args:
return user.is_zb_user() request (HttpRequest): The incoming request
id (str): Intervention's id
def _get_redirect_url(self, *args, **kwargs): Returns:
redirect_url = super()._get_redirect_url(*args, **kwargs)
redirect_url += "#related_data" """
return redirect_url intervention = get_object_or_404(Intervention, id=id)
form = CheckModalForm(request.POST or None, instance=intervention, request=request)
return form.process_request(
request,
msg_success=_("Check performed"),
msg_error=INTERVENTION_INVALID
)
@method_decorator(registration_office_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, *args, **kwargs)
@method_decorator(registration_office_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, *args, **kwargs)

View File

@@ -6,35 +6,49 @@ Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest, Http404, HttpResponse
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
from compensation.models import Compensation
from intervention.forms.modals.remove import RemoveCompensationFromInterventionModalForm
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import shared_access_required
from konova.forms.modals import RemoveModalForm
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from konova.views.remove import BaseRemoveModalFormView
class RemoveCompensationFromInterventionView(LoginRequiredMixin, BaseRemoveModalFormView): class RemoveCompensationFromInterventionView(LoginRequiredMixin, View):
_MODEL_CLS = Intervention
_FORM_CLS = RemoveCompensationFromInterventionModalForm
_MSG_SUCCESS = COMPENSATION_REMOVED_TEMPLATE
_REDIRECT_URL = "intervention:detail"
def _user_has_shared_access(self, user, **kwargs): def __process_request(self, request: HttpRequest, id: str, comp_id: str, *args, **kwargs) -> HttpResponse:
compensation_id = kwargs.get("comp_id", None) """ Renders a modal view for removing the compensation
compensation = get_object_or_404(Compensation, id=compensation_id)
return compensation.is_shared_with(user)
def _user_has_permission(self, user, **kwargs): Args:
return user.is_default_user() request (HttpRequest): The incoming request
id (str): The compensation's id
def _get_msg_success(self, *args, **kwargs): Returns:
compensation_id = kwargs.get("comp_id", None)
compensation = get_object_or_404(Compensation, id=compensation_id)
return self._MSG_SUCCESS.format(compensation.identifier)
def _get_redirect_url(self, *args, **kwargs): """
obj = kwargs.get("obj") intervention = get_object_or_404(Intervention, id=id)
return reverse(self._REDIRECT_URL, args=(obj.id,)) + "#related_data" try:
comp = intervention.compensations.get(
id=comp_id
)
except ObjectDoesNotExist:
raise Http404("Unknown compensation")
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request=request,
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data",
)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request, id: str, comp_id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, comp_id, *args, **kwargs)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request, id: str, comp_id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, comp_id, *args, **kwargs)

View File

@@ -5,27 +5,51 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from intervention.models import Intervention from intervention.models import Intervention
from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_EDITED, DEDUCTION_REMOVED from konova.decorators import default_group_required, shared_access_required
from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView
_INTERVENTION_DETAIL_URL_NAME = "intervention:detail"
class NewInterventionDeductionView(LoginRequiredMixin, AbstractNewDeductionView): class NewInterventionDeductionView(AbstractNewDeductionView):
_MODEL_CLS = Intervention def _custom_check(self, obj):
_MSG_SUCCESS = DEDUCTION_ADDED pass
_REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME
model = Intervention
redirect_url = "intervention:detail"
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditInterventionDeductionView(LoginRequiredMixin, AbstractEditDeductionView): class EditInterventionDeductionView(AbstractEditDeductionView):
_MODEL_CLS = Intervention def _custom_check(self, obj):
_MSG_SUCCESS = DEDUCTION_EDITED pass
_REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME
model = Intervention
redirect_url = "intervention:detail"
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveInterventionDeductionView(LoginRequiredMixin, AbstractRemoveDeductionView): class RemoveInterventionDeductionView(AbstractRemoveDeductionView):
_MODEL_CLS = Intervention def _custom_check(self, obj):
_MSG_SUCCESS = DEDUCTION_REMOVED pass
_REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME
model = Intervention
redirect_url = "intervention:detail"
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,79 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from django.contrib import messages
from django.http import HttpResponse
from django.shortcuts import get_object_or_404, render
from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, DO_NOT_FORGET_TO_SHARE
from konova.views.detail import AbstractDetailView
class DetailInterventionView(AbstractDetailView):
_TEMPLATE = "intervention/detail/view.html"
def get(self, request, id: str, *args, **kwargs) -> HttpResponse:
# Fetch data, filter out deleted related data
intervention = get_object_or_404(
Intervention.objects.select_related(
"geometry",
"legal",
"responsible",
).prefetch_related(
"legal__revocations",
),
id=id,
deleted=None
)
compensations = intervention.compensations.filter(
deleted=None,
)
_user = request.user
is_data_shared = intervention.is_shared_with(user=_user)
geom_form = SimpleGeomForm(
instance=intervention,
)
last_checked = intervention.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
last_checked.get_timestamp_str_formatted(),
last_checked.user
)
has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists()
requesting_user_is_only_shared_user = intervention.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = {
"obj": intervention,
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"compensations": compensations,
"is_entry_shared": is_data_shared,
"geom_form": geom_form,
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": intervention.get_LANIS_link(),
"has_payment_without_document": has_payment_without_document,
TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}",
}
request = intervention.set_status_messages(request)
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)

View File

@@ -5,33 +5,59 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from intervention.forms.modals.document import NewInterventionDocumentModalForm, EditInterventionDocumentModalForm, \ from django.contrib.auth.decorators import login_required
RemoveInterventionDocumentModalForm from django.utils.decorators import method_decorator
from intervention.forms.modals.document import NewInterventionDocumentModalForm
from intervention.models import Intervention, InterventionDocument from intervention.models import Intervention, InterventionDocument
from konova.decorators import default_group_required, shared_access_required
from konova.forms.modals import EditDocumentModalForm
from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \
AbstractEditDocumentView AbstractEditDocumentView
class NewInterventionDocumentView(AbstractNewDocumentView): class NewInterventionDocumentView(AbstractNewDocumentView):
_MODEL_CLS = Intervention model = Intervention
_DOCUMENT_MODEL = InterventionDocument form = NewInterventionDocumentModalForm
_FORM_CLS = NewInterventionDocumentModalForm redirect_url = "intervention:detail"
_REDIRECT_URL = "intervention:detail"
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class GetInterventionDocumentView(AbstractGetDocumentView): class GetInterventionDocumentView(AbstractGetDocumentView):
_MODEL_CLS = Intervention model = Intervention
_DOCUMENT_CLS = InterventionDocument document_model = InterventionDocument
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveInterventionDocumentView(AbstractRemoveDocumentView): class RemoveInterventionDocumentView(AbstractRemoveDocumentView):
_MODEL_CLS = Intervention model = Intervention
_DOCUMENT_CLS = InterventionDocument document_model = InterventionDocument
_FORM_CLS = RemoveInterventionDocumentModalForm
_REDIRECT_URL = "intervention:detail" @method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditInterventionDocumentView(AbstractEditDocumentView): class EditInterventionDocumentView(AbstractEditDocumentView):
_MODEL_CLS = Intervention model = Intervention
_DOCUMENT_CLS = InterventionDocument document_model = InterventionDocument
_FORM_CLS = EditInterventionDocumentModalForm form = EditDocumentModalForm
_REDIRECT_URL = "intervention:detail" redirect_url = "intervention:detail"
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -8,9 +8,11 @@ Created on: 19.08.22
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, render, redirect
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View
from intervention.forms.intervention import EditInterventionForm, NewInterventionForm from intervention.forms.intervention import EditInterventionForm, NewInterventionForm
from intervention.models import Intervention from intervention.models import Intervention
@@ -19,132 +21,189 @@ from konova.contexts import BaseContext
from konova.decorators import default_group_required, shared_access_required from konova.decorators import default_group_required, shared_access_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, \
CHECK_STATE_RESET, FORM_INVALID, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, \
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \ GEOMETRIES_IGNORED_TEMPLATE
BaseEditSpatialLocatedObjectFormView from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.detail import BaseDetailView from konova.views.index import AbstractIndexView
from konova.views.remove import BaseRemoveModalFormView
class InterventionIndexView(LoginRequiredMixin, BaseIndexView): class IndexInterventionView(AbstractIndexView):
_INDEX_TABLE_CLS = InterventionTable def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
_TAB_TITLE = _("Interventions - Overview") """
Renders the index view for Interventions
def _get_queryset(self): Args:
qs = Intervention.objects.filter( request (HttpRequest): The incoming request
deleted=None,
Returns:
A rendered view
"""
# Filtering by user access is performed in table filter inside InterventionTableFilter class
interventions = Intervention.objects.filter(
deleted=None, # not deleted
).select_related( ).select_related(
"legal" "legal"
).order_by( ).order_by(
"-modified__timestamp" "-modified__timestamp"
) )
return qs table = InterventionTable(
request=request,
queryset=interventions
class NewInterventionFormView(BaseNewSpatialLocatedObjectFormView):
_MODEL_CLS = Intervention
_FORM_CLS = NewInterventionForm
_TEMPLATE = "intervention/form/view.html"
_REDIRECT_URL = "intervention:detail"
_TAB_TITLE = _("New intervention")
class EditInterventionFormView(BaseEditSpatialLocatedObjectFormView):
_MODEL_CLS = Intervention
_FORM_CLS = EditInterventionForm
_TEMPLATE = "intervention/form/view.html"
_REDIRECT_URL = "intervention:detail"
_TAB_TITLE = _("Edit {}")
class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
_MODEL_CLS = Intervention
_REDIRECT_URL = "intervention:index"
class InterventionDetailView(BaseDetailView):
_MODEL_CLS = Intervention
_TEMPLATE = "intervention/detail/view.html"
def _get_object(self, id: str):
""" Returns the intervention
Args:
id (str): The intervention's id
Returns:
obj (Intervention): The intervention
"""
# Fetch data, filter out deleted related data
obj = get_object_or_404(
self._MODEL_CLS.objects.select_related(
"geometry",
"legal",
"responsible",
).prefetch_related(
"legal__revocations",
),
id=id,
deleted=None
) )
return obj
def _get_detail_context(self, obj: Intervention):
""" Generate object specific detail context for view
Args:
obj (): The record
Returns:
"""
compensations = obj.compensations.filter(deleted=None)
last_checked = obj.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
last_checked.get_timestamp_str_formatted(),
last_checked.user
)
has_payment_without_document = obj.payments.exists() and not obj.get_documents()[1].exists()
context = { context = {
"last_checked": last_checked, "table": table,
"last_checked_tooltip": last_checked_tooltip, TAB_TITLE_IDENTIFIER: _("Interventions - Overview"),
"compensations": compensations,
"has_payment_without_document": has_payment_without_document,
} }
return context context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@login_required class NewInterventionView(LoginRequiredMixin, View):
@default_group_required _TEMPLATE = "intervention/form/view.html"
@shared_access_required(Intervention, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing interventions
Args: @method_decorator(default_group_required)
request (HttpRequest): The incoming request def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
Returns: """
Renders a view for a new intervention creation
""" Args:
template = "intervention/form/view.html" request (HttpRequest): The incoming request
# 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 Returns:
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": data_form = NewInterventionForm()
geom_form = SimpleGeomForm(read_only=False)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New intervention"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(default_group_required)
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""
Renders a view for a new intervention creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
data_form = NewInterventionForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
intervention = data_form.save(request.user, geom_form)
if generated_identifier != intervention.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
intervention.identifier
)
)
messages.success(request, _("Intervention {} added").format(intervention.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("intervention:detail", id=intervention.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger", )
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New intervention"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
class InterventionIdentifierGeneratorView(AbstractIdentifierGeneratorView):
_MODEL = Intervention
class EditInterventionView(LoginRequiredMixin, View):
_TEMPLATE = "intervention/form/view.html"
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
"""
Renders a view for editing interventions
Args:
request (HttpRequest): The incoming request
id (str): The intervention identifier
Returns:
HttpResponse: The rendered view
"""
# 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)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
"""
Process saved form content
Args:
request (HttpRequest): The incoming request
id (str): The intervention id
Returns:
HttpResponse:
"""
# 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 data_form.is_valid() and geom_form.is_valid(): 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 # 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 # Save the current state of recorded|checked to inform the user in case of a status reset due to editing
@@ -158,28 +217,17 @@ def edit_view(request: HttpRequest, id: str):
request, request,
GEOMETRY_SIMPLIFIED GEOMETRY_SIMPLIFIED
) )
num_ignored_geometries = geom_form.get_num_geometries_ignored() num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0: if num_ignored_geometries > 0:
messages.info( messages.info(
request, request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
) )
return redirect("intervention:detail", id=intervention.id) return redirect("intervention:detail", id=intervention.id)
else: context = {
messages.error(request, FORM_INVALID, extra_tags="danger",) "form": data_form,
else: "geom_form": geom_form,
# For clarification: nothing in this case TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier),
pass }
context = { context = BaseContext(request, context).context
"form": data_form, return render(request, self._TEMPLATE, context)
"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

@@ -5,11 +5,19 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import shared_access_required, default_group_required
from konova.views.log import AbstractLogView from konova.views.log import AbstractLogView
class InterventionLogView(LoginRequiredMixin, AbstractLogView): class InterventionLogView(AbstractLogView):
_MODEL_CLS = Intervention model = Intervention
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,12 +5,19 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import conservation_office_group_required, shared_access_required
from konova.views.record import AbstractRecordView from konova.views.record import AbstractRecordView
class InterventionRecordView(LoginRequiredMixin, AbstractRecordView): class InterventionRecordView(AbstractRecordView):
_MODEL_CLS = Intervention model = Intervention
_REDIRECT_URL = "intervention:detail"
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -0,0 +1,20 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from django.http import HttpRequest, HttpResponse
from django.utils.decorators import method_decorator
from intervention.models import Intervention
from konova.decorators import shared_access_required
from konova.views.remove import AbstractRemoveView
class RemoveInterventionView(AbstractRemoveView):
_MODEL = Intervention
_REDIRECT_URL = "intervention:index"
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
return super().get(request, *args, **kwargs)

View File

@@ -5,41 +5,78 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from intervention.models import Intervention from intervention.models import Intervention
from konova.sub_settings.django_settings import BASE_URL from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.qrcode import QrCode from konova.utils.qrcode import QrCode
from konova.views.report import BaseReportView from konova.views.report import AbstractPublicReportView
class InterventionReportView(BaseReportView): class InterventionPublicReportView(AbstractPublicReportView):
_TEMPLATE = 'intervention/report/report.html' _TEMPLATE = "intervention/report/report.html"
_MODEL = Intervention
def _get_report_context(self, obj: Intervention): def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" Returns the specific context needed for an intervention report """ Renders the public report view
Args: Args:
obj (Intervention): The object for the report request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns: Returns:
dict: The object specific context for rendering the report
"""
distinct_deductions = obj.deductions.all().distinct("account")
report_url = BASE_URL + reverse("intervention:report", args=(obj.id,))
qrcode_report = QrCode(report_url, 10)
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
return { """
intervention = get_object_or_404(Intervention, id=id)
tab_title = _("Report {}").format(intervention.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not intervention.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=intervention
)
parcels = intervention.get_underlying_parcels()
distinct_deductions = intervention.deductions.all().distinct(
"account"
)
qrcode = QrCode(
content=request.build_absolute_uri(reverse("intervention:report", args=(id,))),
size=10
)
qrcode_lanis = QrCode(
content=intervention.get_LANIS_link(),
size=7
)
context = {
"obj": intervention,
"deductions": distinct_deductions, "deductions": distinct_deductions,
"qrcode": { "qrcode": {
"img": qrcode_report.get_img(), "img": qrcode.get_img(),
"url": qrcode_report.get_content(), "url": qrcode.get_content(),
}, },
"qrcode_lanis": { "qrcode_lanis": {
"img": qrcode_lanis.get_img(), "img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(), "url": qrcode_lanis.get_content(),
}, },
"geom_form": geom_form,
"parcels": parcels,
"tables_scrollable": False, "tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)

View File

@@ -5,12 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from intervention.forms.modals.resubmission import InterventionResubmissionModalForm from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import default_group_required, shared_access_required, login_required_modal
from konova.views.resubmission import AbstractResubmissionView from konova.views.resubmission import AbstractResubmissionView
class InterventionResubmissionView(AbstractResubmissionView): class InterventionResubmissionView(AbstractResubmissionView):
_MODEL_CLS = Intervention model = Intervention
_FORM_CLS = InterventionResubmissionModalForm redirect_url_base = "intervention:detail"
_REDIRECT_URL = "intervention:detail" form_action_url_base = "intervention:resubmission-create"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -7,52 +7,65 @@ Created on: 19.08.22
""" """
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \ from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \
RemoveRevocationModalForm RemoveRevocationModalForm
from intervention.models import Intervention, RevocationDocument from intervention.models import Intervention, RevocationDocument, Revocation
from konova.decorators import default_group_required, shared_access_required, login_required_modal
from konova.utils.documents import get_document from konova.utils.documents import get_document
from konova.utils.message_templates import DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED, REVOCATION_ADDED from konova.utils.message_templates import REVOCATION_ADDED, DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED
from konova.views.base import BaseModalFormView, BaseView
class BaseRevocationView(LoginRequiredMixin, BaseModalFormView): class NewInterventionRevocationView(LoginRequiredMixin, View):
_MODEL_CLS = Intervention def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
_REDIRECT_URL = "intervention:detail" """ Renders sharing form for an intervention
class Meta: Args:
abstract = True request (HttpRequest): The incoming request
id (str): Intervention's id
def _user_has_permission(self, user, **kwargs): Returns:
return user.is_default_user()
def _get_redirect_url(self, *args, **kwargs): """
url = super()._get_redirect_url(*args, **kwargs) intervention = get_object_or_404(Intervention, id=id)
return f"{url}#related_data" form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention,
request=request)
return form.process_request(
request,
msg_success=REVOCATION_ADDED,
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, *args, **kwargs)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, *args, **kwargs)
class NewRevocationView(BaseRevocationView): class GetInterventionRevocationView(LoginRequiredMixin, View):
_FORM_CLS = NewRevocationModalForm @method_decorator(default_group_required)
_MSG_SUCCESS = REVOCATION_ADDED def get(self, request: HttpRequest, doc_id: str, *args, **kwargs) -> HttpResponse:
""" Returns the revocation document as downloadable file
Wraps the generic document fetcher function from konova.utils.
class EditRevocationView(BaseRevocationView): Args:
_FORM_CLS = EditRevocationModalForm request (HttpRequest): The incoming request
_MSG_SUCCESS = REVOCATION_EDITED doc_id (str): The document id
Returns:
class RemoveRevocationView(BaseRevocationView): """
_FORM_CLS = RemoveRevocationModalForm
_MSG_SUCCESS = REVOCATION_REMOVED
class GetRevocationDocumentView(LoginRequiredMixin, BaseView):
_MODEL_CLS = RevocationDocument
_REDIRECT_URL = "intervention:detail"
def get(self, request: HttpRequest, doc_id: str):
doc = get_object_or_404(RevocationDocument, id=doc_id) doc = get_object_or_404(RevocationDocument, id=doc_id)
# File download only possible if related instance is shared with user # File download only possible if related instance is shared with user
if not doc.instance.legal.intervention.users.filter(id=request.user.id): if not doc.instance.legal.intervention.users.filter(id=request.user.id):
@@ -63,14 +76,70 @@ class GetRevocationDocumentView(LoginRequiredMixin, BaseView):
return redirect("intervention:detail", id=doc.instance.id) return redirect("intervention:detail", id=doc.instance.id)
return get_document(doc) return get_document(doc)
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
def _user_has_shared_access(self, user, **kwargs): class EditInterventionRevocationView(LoginRequiredMixin, View):
obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get("doc_id")) def __process_request(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
assert obj is not None """ Renders a edit view for a revocation
return obj.instance.intervention.is_shared_with(user)
def _get_redirect_url(self, *args, **kwargs): Args:
url = super()._get_redirect_url(*args, **kwargs) request (HttpRequest): The incoming request
return f"{url}#related_data" id (str): The intervention's id as string
revocation_id (str): The revocation's id as string
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
revocation = get_object_or_404(Revocation, id=revocation_id)
form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention,
revocation=revocation, request=request)
return form.process_request(
request,
REVOCATION_EDITED,
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, revocation_id, *args, **kwargs)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, revocation_id, *args, **kwargs)
class RemoveInterventionRevocationView(LoginRequiredMixin, View):
def __process_request(self, request, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
""" Renders a remove view for a revocation
Args:
request (HttpRequest): The incoming request
id (str): The intervention's id as string
revocation_id (str): The revocation's id as string
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
revocation = get_object_or_404(Revocation, id=revocation_id)
form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation,
request=request)
return form.process_request(
request,
REVOCATION_REMOVED,
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, revocation_id, *args, **kwargs)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, revocation_id, *args, **kwargs)

View File

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

View File

@@ -10,18 +10,21 @@ from abc import abstractmethod
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount
from konova.models import BaseObject
class BaseForm(forms.Form): class BaseForm(forms.Form):
""" """
Basic form for that holds attributes needed in all other forms Basic form for that holds attributes needed in all other forms
""" """
template = None
action_url = None action_url = None
action_btn_label = _("Save") action_btn_label = _("Save")
form_title = None form_title = None
cancel_redirect = None cancel_redirect = None
form_caption = None form_caption = None
instance = None # The data holding model object instance = None # The data holding model object
user = None # The performing user
request = None request = None
form_attrs = {} # Holds additional attributes, that can be used in the template form_attrs = {} # Holds additional attributes, that can be used in the template
has_required_fields = False # Automatically set. Triggers hint rendering in templates has_required_fields = False # Automatically set. Triggers hint rendering in templates
@@ -30,7 +33,6 @@ class BaseForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.instance = kwargs.pop("instance", None) self.instance = kwargs.pop("instance", None)
self.user = kwargs.pop("user", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.request is not None: if self.request is not None:
self.user = self.request.user self.user = self.request.user
@@ -40,10 +42,11 @@ class BaseForm(forms.Form):
self.has_required_fields = True self.has_required_fields = True
break break
self.check_for_recorded_instance()
self.__check_valid_label_input_ratio() self.__check_valid_label_input_ratio()
@abstractmethod @abstractmethod
def save(self, *arg, **kwargs): def save(self):
# To be implemented in subclasses! # To be implemented in subclasses!
pass pass
@@ -133,3 +136,34 @@ class BaseForm(forms.Form):
set_class = self.fields[field].widget.attrs.get("class", "") set_class = self.fields[field].widget.attrs.get("class", "")
set_class = set_class.replace(cls, "") set_class = set_class.replace(cls, "")
self.fields[field].widget.attrs["class"] = set_class 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

@@ -35,6 +35,7 @@ class SimpleGeomForm(BaseForm):
disabled=False, disabled=False,
) )
_num_geometries_ignored: int = 0 _num_geometries_ignored: int = 0
empty = False
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.read_only = kwargs.pop("read_only", True) self.read_only = kwargs.pop("read_only", True)
@@ -49,11 +50,11 @@ class SimpleGeomForm(BaseForm):
raise AttributeError raise AttributeError
geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP) geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
self._set_geojson_properties(geojson, title=self.instance.identifier or None) geojson = self._set_geojson_properties(geojson, title=self.instance.identifier or None)
geom = json.dumps(geojson) geom = json.dumps(geojson)
except AttributeError: except AttributeError:
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level # If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
geom = "" geom = json.dumps({})
self.empty = True self.empty = True
self.initialize_form_field("output", geom) self.initialize_form_field("output", geom)
@@ -62,17 +63,17 @@ class SimpleGeomForm(BaseForm):
super().is_valid() super().is_valid()
is_valid = True is_valid = True
# Get geojson from form # Make sure invalid geometry is properly rendered again to the user
geom = self.data.get("output", None) # Therefore: write submitted data back into form field
if geom is None or len(geom) == 0: # (does not matter whether we know if it is valid or invalid)
# empty geometry is a valid geometry submitted_data = self.data["output"]
self.cleaned_data["output"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt submitted_data = json.loads(submitted_data)
return is_valid submitted_data = self._set_geojson_properties(submitted_data)
geom = json.loads(geom) self.initialize_form_field("output", json.dumps(submitted_data))
# Write submitted data back into form field to make sure invalid geometry # Get geojson from form for validity checking
# will be rendered again on failed submit geom = self.data.get("output", json.dumps({}))
self.initialize_form_field("output", self.data["output"]) geom = json.loads(geom)
# Initialize features list with empty MultiPolygon, so that an empty input will result in a # Initialize features list with empty MultiPolygon, so that an empty input will result in a
# proper empty MultiPolygon object # proper empty MultiPolygon object
@@ -84,20 +85,23 @@ class SimpleGeomForm(BaseForm):
"MultiPolygon", "MultiPolygon",
"MultiPolygon25D", "MultiPolygon25D",
] ]
# Check validity for each feature of the geometry
for feature in features_json: for feature in features_json:
feature_geom = feature.get("geometry", feature) feature_geom = feature.get("geometry", feature)
if feature_geom is None: if feature_geom is None:
# Fallback for rare cases where a feature does not contain any geometry # Fallback for rare cases where a feature does not contain any geometry
continue continue
# Try to create a geometry object from the single feature
feature_geom = json.dumps(feature_geom) feature_geom = json.dumps(feature_geom)
g = gdal.OGRGeometry(feature_geom, srs=DEFAULT_SRID_RLP) g = gdal.OGRGeometry(feature_geom, srs=DEFAULT_SRID_RLP)
flatten_geometry = g.coord_dim > 2 geometry_has_unwanted_dimensions = g.coord_dim > 2
if flatten_geometry: if geometry_has_unwanted_dimensions:
g = self.__flatten_geom_to_2D(g) g = self.__flatten_geom_to_2D(g)
if g.geom_type not in accepted_ogr_types: geometry_type_is_accepted = g.geom_type not in accepted_ogr_types
if geometry_type_is_accepted:
self.add_error("output", _("Only surfaces allowed. Points or lines must be buffered.")) self.add_error("output", _("Only surfaces allowed. Points or lines must be buffered."))
is_valid &= False is_valid &= False
return is_valid return is_valid
@@ -109,27 +113,33 @@ class SimpleGeomForm(BaseForm):
self._num_geometries_ignored += 1 self._num_geometries_ignored += 1
continue continue
# Whatever this geometry object is -> try to create a Polygon from it
# The resulting polygon object automatically detects whether a valid polygon has been created or not
g = Polygon.from_ewkt(g.ewkt) g = Polygon.from_ewkt(g.ewkt)
is_valid &= g.valid is_valid &= g.valid
if not g.valid: if not g.valid:
self.add_error("output", g.valid_reason) self.add_error("output", g.valid_reason)
return is_valid return is_valid
# If the resulting polygon is just a single polygon, we add it to the list of properly casted features
if isinstance(g, Polygon): if isinstance(g, Polygon):
features.append(g) features.append(g)
elif isinstance(g, MultiPolygon): elif isinstance(g, MultiPolygon):
# The resulting polygon could be of type MultiPolygon (due to multiple surfaces)
# If so, we extract all polygons from the MultiPolygon and extend the casted features list
features.extend(list(g)) features.extend(list(g))
# Unionize all geometry features into one new MultiPolygon # Unionize all polygon features into one new MultiPolygon
if features: if features:
form_geom = MultiPolygon(*features, srid=DEFAULT_SRID_RLP).unary_union form_geom = MultiPolygon(*features, srid=DEFAULT_SRID_RLP).unary_union
else: else:
# If no features have been processed, this indicates an empty geometry - so we store an empty geometry
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP) form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided. # Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
form_geom = Geometry.cast_to_multipolygon(form_geom) form_geom = Geometry.cast_to_multipolygon(form_geom)
# Write unioned Multipolygon into cleaned data # Write unionized Multipolygon back into cleaned data
if self.cleaned_data is None: if self.cleaned_data is None:
self.cleaned_data = {} self.cleaned_data = {}
self.cleaned_data["output"] = form_geom.ewkt self.cleaned_data["output"] = form_geom.ewkt
@@ -252,6 +262,8 @@ class SimpleGeomForm(BaseForm):
""" """
features = geojson.get("features", []) features = geojson.get("features", [])
for feature in features: for feature in features:
if not feature.get("properties", None):
feature["properties"] = {}
feature["properties"]["editable"] = not self.read_only feature["properties"]["editable"] = not self.read_only
if title: if title:
feature["properties"]["title"] = title feature["properties"]["title"] = title

View File

@@ -23,7 +23,7 @@ class BaseModalForm(BaseForm, BSModalForm):
""" """
is_modal_form = True is_modal_form = True
render_submit = True render_submit = True
_TEMPLATE = "modal/modal_form.html" template = "modal/modal_form.html"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*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") 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 request.method == "POST":
if self.is_valid(): if self.is_valid():
if not is_ajax(request.META): if not is_ajax(request.META):

View File

@@ -8,10 +8,10 @@ Created on: 15.08.22
from django import forms from django import forms
from django.db import transaction from django.db import transaction
from django.db.models.fields.files import FieldFile from django.db.models.fields.files import FieldFile
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from konova.forms.modals.base_form import BaseModalForm from konova.forms.modals.base_form import BaseModalForm
from konova.models import AbstractDocument
from konova.utils import validators from konova.utils import validators
from konova.utils.message_templates import DOCUMENT_EDITED, FILE_SIZE_TOO_LARGE, FILE_TYPE_UNSUPPORTED from konova.utils.message_templates import DOCUMENT_EDITED, FILE_SIZE_TOO_LARGE, FILE_TYPE_UNSUPPORTED
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -69,7 +69,7 @@ class NewDocumentModalForm(BaseModalForm):
} }
) )
) )
_DOCUMENT_CLS = None document_model = None
class Meta: class Meta:
abstract = True abstract = True
@@ -81,7 +81,7 @@ class NewDocumentModalForm(BaseModalForm):
self.form_attrs = { self.form_attrs = {
"enctype": "multipart/form-data", # important for file upload "enctype": "multipart/form-data", # important for file upload
} }
if not self._DOCUMENT_CLS: if not self.document_model:
raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__)) raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__))
def is_valid(self): def is_valid(self):
@@ -93,14 +93,14 @@ class NewDocumentModalForm(BaseModalForm):
# FieldFile declares that no new file has been uploaded and we do not need to check on the file again # FieldFile declares that no new file has been uploaded and we do not need to check on the file again
return super_valid return super_valid
mime_type_valid = self._DOCUMENT_CLS.is_mime_type_valid(_file) mime_type_valid = self.document_model.is_mime_type_valid(_file)
if not mime_type_valid: if not mime_type_valid:
self.add_error( self.add_error(
"file", "file",
FILE_TYPE_UNSUPPORTED FILE_TYPE_UNSUPPORTED
) )
file_size_valid = self._DOCUMENT_CLS.is_file_size_valid(_file) file_size_valid = self.document_model.is_file_size_valid(_file)
if not file_size_valid: if not file_size_valid:
self.add_error( self.add_error(
"file", "file",
@@ -115,7 +115,7 @@ class NewDocumentModalForm(BaseModalForm):
action = UserActionLogEntry.get_created_action(self.user) action = UserActionLogEntry.get_created_action(self.user)
edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document")) edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document"))
doc = self._DOCUMENT_CLS.objects.create( doc = self.document_model.objects.create(
created=action, created=action,
title=self.cleaned_data["title"], title=self.cleaned_data["title"],
comment=self.cleaned_data["comment"], comment=self.cleaned_data["comment"],
@@ -133,12 +133,10 @@ class NewDocumentModalForm(BaseModalForm):
class EditDocumentModalForm(NewDocumentModalForm): class EditDocumentModalForm(NewDocumentModalForm):
document = None document = None
_DOCUMENT_CLS = None document_model = AbstractDocument
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
doc_id = kwargs.pop("doc_id", None) self.document = kwargs.pop("document", None)
self.document = get_object_or_404(self._DOCUMENT_CLS, id=doc_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit document") self.form_title = _("Edit document")
form_data = { form_data = {

View File

@@ -6,11 +6,10 @@ Created on: 15.08.22
""" """
from django import forms from django import forms
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from konova.forms.modals.base_form import BaseModalForm from konova.forms.modals.base_form import BaseModalForm
from konova.models import BaseObject, Deadline from konova.models import BaseObject
class RemoveModalForm(BaseModalForm): class RemoveModalForm(BaseModalForm):
@@ -52,19 +51,9 @@ class RemoveDeadlineModalForm(RemoveModalForm):
deadline = None deadline = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
deadline_id = kwargs.pop("deadline_id", None) deadline = kwargs.pop("deadline", None)
self.deadline = get_object_or_404(Deadline, id=deadline_id) self.deadline = deadline
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def save(self): def save(self):
self.instance.remove_deadline(self) self.instance.remove_deadline(self)
class RemoveDocumentModalForm(RemoveModalForm):
instance = None
_DOCUMENT_CLS = None
def __init__(self, *args, **kwargs):
document_id = kwargs.pop("doc_id", None)
super().__init__(*args, **kwargs)
self.instance = get_object_or_404(self._DOCUMENT_CLS, id=document_id)

View File

@@ -10,6 +10,7 @@ import json
from django.contrib.gis.db.models import MultiPolygonField from django.contrib.gis.db.models import MultiPolygonField
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from django.contrib.gis.geos import MultiPolygon from django.contrib.gis.geos import MultiPolygon
@@ -102,24 +103,41 @@ class Geometry(BaseResource):
resolved_conflicts = all_conflicted_by_conflicts.exclude(id__in=still_conflicting_conflicts) resolved_conflicts = all_conflicted_by_conflicts.exclude(id__in=still_conflicting_conflicts)
resolved_conflicts.delete() resolved_conflicts.delete()
def get_data_objects(self): def get_data_objects(self, limit_to_attrs: list = None):
""" Getter for all objects which are related to this geometry """ Getter for all objects which are related to this geometry
Using the limit_to_attrs we can limit the amount of returned data directly onto the data object attributes
we want to have. Reduces memory consumption and runtime.
Returns: Returns:
objs (list): The list of objects objs (list): The list of objects
""" """
objs = [] objs = []
sets = [
# Some related data sets can be processed rather easily
regular_sets = [
self.intervention_set, self.intervention_set,
self.compensation_set,
self.ema_set, self.ema_set,
self.ecoaccount_set, self.ecoaccount_set,
] ]
for _set in sets: for _set in regular_sets:
set_objs = _set.filter( set_objs = _set.filter(
deleted=None deleted=None
) )
objs += set_objs if limit_to_attrs:
objs += set_objs.values_list(*limit_to_attrs, flat=True)
else:
objs += set_objs
# ... but we need a special treatment for compensations, since they can be deleted directly OR inherit their
# de-facto-deleted status from their deleted parent intervention
comp_objs = self.compensation_set.filter(
Q(deleted=None) & Q(intervention__deleted=None)
)
if limit_to_attrs:
objs += comp_objs.values_list(*limit_to_attrs, flat=True)
else:
objs += comp_objs
return objs return objs
def get_data_object(self): def get_data_object(self):
@@ -397,7 +415,10 @@ class Geometry(BaseResource):
""" """
output_geom = input_geom output_geom = input_geom
if not isinstance(input_geom, MultiPolygon): if not isinstance(input_geom, MultiPolygon):
output_geom = MultiPolygon(input_geom, srid=DEFAULT_SRID_RLP) try:
output_geom = MultiPolygon(input_geom, srid=DEFAULT_SRID_RLP)
except TypeError as e:
raise AssertionError(f"Only (Multi)Polygon allowed! Could not convert {input_geom.geom_type} to MultiPolygon")
return output_geom return output_geom
@staticmethod @staticmethod

View File

@@ -677,19 +677,23 @@ class GeoReferencedMixin(models.Model):
return request return request
instance_objs = [] instance_objs = []
conflicts = self.geometry.conflicts_geometries.all() needed_data_object_attrs = [
"identifier"
]
conflicts = self.geometry.conflicts_geometries.iterator()
for conflict in conflicts: for conflict in conflicts:
instance_objs += conflict.affected_geometry.get_data_objects() # Only check the affected geometry of this conflict, since we know the conflicting geometry is self.geometry
instance_objs += conflict.affected_geometry.get_data_objects(needed_data_object_attrs)
conflicts = self.geometry.conflicted_by_geometries.all() conflicts = self.geometry.conflicted_by_geometries.iterator()
for conflict in conflicts: for conflict in conflicts:
instance_objs += conflict.conflicting_geometry.get_data_objects() # Only check the conflicting geometry of this conflict, since we know the affected geometry is self.geometry
instance_objs += conflict.conflicting_geometry.get_data_objects(needed_data_object_attrs)
add_message = len(instance_objs) > 0 add_message = len(instance_objs) > 0
if add_message: if add_message:
instance_identifiers = [x.identifier for x in instance_objs] instance_identifiers = ", ".join(instance_objs)
instance_identifiers = ", ".join(instance_identifiers)
message_str = GEOMETRY_CONFLICT_WITH_TEMPLATE.format(instance_identifiers) message_str = GEOMETRY_CONFLICT_WITH_TEMPLATE.format(instance_identifiers)
messages.info(request, message_str) messages.info(request, message_str)
return request return request

View File

@@ -291,5 +291,5 @@ Overwrites netgis.css attributes
} }
.netgis-menu{ .netgis-menu{
z-index: 100 !important; z-index: 1 !important;
} }

View File

@@ -11,4 +11,4 @@ BASE_TITLE = "KSP - Kompensationsverzeichnis Service Portal"
BASE_FRONTEND_TITLE = "Kompensationsverzeichnis Service Portal" BASE_FRONTEND_TITLE = "Kompensationsverzeichnis Service Portal"
TAB_TITLE_IDENTIFIER = "tab_title" TAB_TITLE_IDENTIFIER = "tab_title"
HELP_LINK = "https://dienste.naturschutz.rlp.de/doku/doku.php?id=ksp2:start" HELP_LINK = "https://dienste.naturschutz.rlp.de/doku/doku.php?id=ksp2:start"
IMPRESSUM_LINK = "https://naturschutz.rlp.de/index.php?q=impressum" IMPRESSUM_LINK = "https://naturschutz.rlp.de/ueber-uns/impressum"

View File

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

View File

@@ -103,7 +103,7 @@ class EditDeadlineModalFormTestCase(NewDeadlineModalFormTestCase):
data, data,
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
deadline_id=self.finished_deadline.id, deadline=self.finished_deadline,
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)

View File

@@ -17,9 +17,9 @@ from django.utils.timezone import now
from compensation.forms.modals.document import NewEcoAccountDocumentModalForm, NewCompensationDocumentModalForm from compensation.forms.modals.document import NewEcoAccountDocumentModalForm, NewCompensationDocumentModalForm
from compensation.models import Payment from compensation.models import Payment
from ema.forms import NewEmaDocumentModalForm from ema.forms import NewEmaDocumentModalForm
from intervention.forms.modals.document import NewInterventionDocumentModalForm, EditInterventionDocumentModalForm from intervention.forms.modals.document import NewInterventionDocumentModalForm
from intervention.models import InterventionDocument from intervention.models import InterventionDocument
from konova.forms.modals import NewDocumentModalForm, RecordModalForm, RemoveModalForm, \ from konova.forms.modals import EditDocumentModalForm, NewDocumentModalForm, RecordModalForm, RemoveModalForm, \
RemoveDeadlineModalForm, ResubmissionModalForm RemoveDeadlineModalForm, ResubmissionModalForm
from konova.models import Resubmission from konova.models import Resubmission
from konova.tests.test_views import BaseTestCase from konova.tests.test_views import BaseTestCase
@@ -106,12 +106,12 @@ class EditDocumentModalFormTestCase(NewDocumentModalFormTestCase):
InterventionDocument, InterventionDocument,
instance=self.intervention instance=self.intervention
) )
self.form = EditInterventionDocumentModalForm( self.form = EditDocumentModalForm(
self.data, self.data,
dummy_file_dict, dummy_file_dict,
request=self.request, request=self.request,
instance=self.intervention, instance=self.intervention,
doc_id=self.doc.id document=self.doc
) )
def test_init(self): def test_init(self):
@@ -122,6 +122,7 @@ class EditDocumentModalFormTestCase(NewDocumentModalFormTestCase):
self.assertEqual(self.form.fields["title"].initial, self.doc.title) self.assertEqual(self.form.fields["title"].initial, self.doc.title)
self.assertEqual(self.form.fields["comment"].initial, self.doc.comment) self.assertEqual(self.form.fields["comment"].initial, self.doc.comment)
self.assertEqual(self.form.fields["creation_date"].initial, self.doc.date_of_creation) self.assertEqual(self.form.fields["creation_date"].initial, self.doc.date_of_creation)
self.assertEqual(self.form.fields["file"].initial, self.doc.file)
def test_save(self): def test_save(self):
self.assertTrue(self.form.is_valid(), msg=self.form.errors) self.assertTrue(self.form.is_valid(), msg=self.form.errors)
@@ -255,7 +256,7 @@ class RemoveDeadlineTestCase(BaseTestCase):
form = RemoveDeadlineModalForm( form = RemoveDeadlineModalForm(
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
deadline_id=self.finished_deadline.id deadline=self.finished_deadline
) )
self.assertEqual(form.form_title, str(_("Remove"))) self.assertEqual(form.form_title, str(_("Remove")))
self.assertEqual(form.form_caption, str(_("Are you sure?"))) self.assertEqual(form.form_caption, str(_("Are you sure?")))
@@ -272,7 +273,7 @@ class RemoveDeadlineTestCase(BaseTestCase):
data, data,
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
deadline_id=self.finished_deadline.id deadline=self.finished_deadline
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)
form.save() form.save()

View File

@@ -7,7 +7,9 @@ Created on: 01.09.21
""" """
from django.http import FileResponse, HttpRequest, Http404 from django.http import FileResponse, HttpRequest, Http404
from konova.forms.modals import RemoveModalForm
from konova.models import AbstractDocument from konova.models import AbstractDocument
from konova.utils.message_templates import DOCUMENT_REMOVED_TEMPLATE
def get_document(doc: AbstractDocument): def get_document(doc: AbstractDocument):
@@ -24,3 +26,28 @@ def get_document(doc: AbstractDocument):
return FileResponse(doc.file, as_attachment=True) return FileResponse(doc.file, as_attachment=True)
except FileNotFoundError: except FileNotFoundError:
raise Http404() raise Http404()
def remove_document(request: HttpRequest, doc: AbstractDocument):
""" Renders a form for uploading new documents
This function works using a modal. We are not using the regular way, the django bootstrap modal forms are
intended to be used. Instead of View classes we work using the classic way of dealing with forms (see below).
It is important to mention, that modal forms, which should reload the page afterwards, must provide a
'reload_page' bool in the context. This way, the modal may reload the page or not.
For further details see the comments in templates/modal or
https://github.com/trco/django-bootstrap-modal-forms
Args:
request (HttpRequest): The incoming request
Returns:
"""
title = doc.title
form = RemoveModalForm(request.POST or None, instance=doc, request=request)
return form.process_request(
request=request,
msg_success=DOCUMENT_REMOVED_TEMPLATE.format(title),
)

View File

@@ -5,6 +5,9 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 11.12.23 Created on: 11.12.23
""" """
import json
from json import JSONDecodeError
from django.views.debug import ExceptionReporter from django.views.debug import ExceptionReporter
@@ -30,7 +33,7 @@ class KonovaExceptionReporter(ExceptionReporter):
""" """
whitelist = [ whitelist = [
"is_email", "is_email",
"unicdoe_hint", "unicode_hint",
"frames", "frames",
"request", "request",
"user_str", "user_str",
@@ -39,6 +42,8 @@ class KonovaExceptionReporter(ExceptionReporter):
"raising_view_name", "raising_view_name",
"exception_type", "exception_type",
"exception_value", "exception_value",
"filtered_GET_items",
"filtered_POST_items",
] ]
clean_data = dict() clean_data = dict()
for entry in whitelist: for entry in whitelist:
@@ -56,7 +61,28 @@ class KonovaExceptionReporter(ExceptionReporter):
""" """
tb_data = super().get_traceback_data() tb_data = super().get_traceback_data()
return_data = tb_data
if self.is_email: if self.is_email:
tb_data = self._filter_traceback_data(tb_data) filtered_data = dict()
filtered_data.update(self._filter_traceback_data(tb_data))
filtered_data.update(self._filter_POST_body(tb_data))
return_data = filtered_data
return return_data
return tb_data def _filter_POST_body(self, tb_data: dict):
""" Filters POST body from traceback data
"""
post_data = tb_data.get("request", None)
if post_data:
post_data = post_data.body
try:
post_data = json.loads(post_data)
except JSONDecodeError:
pass
post_data = {
"filtered_POST_items": [
("body", post_data),
]
}
return post_data

View File

@@ -5,11 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.09.21 Created on: 17.09.21
""" """
from uuid import UUID
from django.contrib import messages
from django.utils.translation import gettext_lazy as _
from django.http import HttpRequest, Http404
def format_german_float(num) -> str: def format_german_float(num) -> str:
@@ -24,27 +19,3 @@ def format_german_float(num) -> str:
num (str): The number as german Gleitkommazahl num (str): The number as german Gleitkommazahl
""" """
return format(num, "0,.2f").replace(",", "X").replace(".", ",").replace("X", ".") return format(num, "0,.2f").replace(",", "X").replace(".", ",").replace("X", ".")
def check_user_is_in_any_group(request: HttpRequest):
"""
Checks for any group membership. Adds a message in case of having none.
"""
user = request.user
# Inform user about missing group privileges!
groups = user.groups.all()
if not groups:
messages.info(
request,
_("+++ Attention: You are not part of any group. You won't be able to create, edit or do anything. Please contact an administrator. +++")
)
return request
def check_id_is_valid_uuid(uuid: str):
if uuid:
try:
# Check whether the id is a proper uuid or something that would break a db fetch
UUID(uuid)
except ValueError:
raise Http404

View File

@@ -7,6 +7,10 @@ Created on: 09.11.20
""" """
import random import random
import string import string
import qrcode
import qrcode.image.svg
from io import BytesIO
def generate_token() -> str: def generate_token() -> str:
@@ -38,3 +42,24 @@ def generate_random_string(length: int, use_numbers: bool = False, use_letters_l
ret_val = "".join(random.choice(elements) for i in range(length)) ret_val = "".join(random.choice(elements) for i in range(length))
return ret_val return ret_val
class IdentifierGenerator:
_MODEL = None
def __init__(self, model):
from konova.models import BaseObject
if not issubclass(model, BaseObject):
raise AssertionError("Model must be a subclass of BaseObject!")
self._MODEL = model
def generate_id(self) -> str:
""" Generates a unique identifier
Returns:
"""
unpersisted_object = self._MODEL()
identifier = unpersisted_object.generate_new_identifier()
while self._MODEL.objects.filter(identifier=identifier).exists():
identifier = unpersisted_object.generate_new_identifier()
return identifier

View File

@@ -19,20 +19,7 @@ IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since ano
ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office users are allowed to remove entries.") ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office users are allowed to remove entries.")
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.") MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
CHECK_STATE_RESET = _("Status of Checked reset") CHECK_STATE_RESET = _("Status of Checked reset")
# USER | TEAM
TEAM_ADDED = _("New team added")
TEAM_EDITED = _("Team edited")
TEAM_REMOVED = _("Team removed")
TEAM_LEFT = _("Left Team")
# REMOVED
GENERIC_REMOVED_TEMPLATE = _("{} removed")
# RECORDING
RECORDED_BLOCKS_EDIT = _("Entry is recorded. To edit data, the entry first needs to be unrecorded.") RECORDED_BLOCKS_EDIT = _("Entry is recorded. To edit data, the entry first needs to be unrecorded.")
ENTRY_RECORDED = _("{} recorded")
ENTRY_UNRECORDED = _("{} unrecorded")
# SHARE # SHARE
DATA_UNSHARED = _("This data is not shared with you") DATA_UNSHARED = _("This data is not shared with you")
@@ -108,6 +95,3 @@ DATA_IS_UNCHECKED = _("Current data not checked yet")
# API TOKEN SETTINGS # API TOKEN SETTINGS
NEW_API_TOKEN_GENERATED = _("New token generated. Administrators need to validate.") NEW_API_TOKEN_GENERATED = _("New token generated. Administrators need to validate.")
# RESUBMISSION
NEW_RESUBMISSION_CREATED = _("Resubmission set")

View File

@@ -1,6 +1,6 @@
""" """
Author: Michel Peltriaux Author: Michel Peltriaux
Created on: 17.10.25 Created on: 14.12.25
""" """
from io import BytesIO from io import BytesIO

Some files were not shown because too many files have changed in this diff Show More