Compare commits

...

11 Commits

Author SHA1 Message Date
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
38 changed files with 1570 additions and 1199 deletions

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

@ -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,30 +7,32 @@ 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 report_view 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 index_view, new_view, new_id_view, detail_view, edit_view, \ from compensation.views.compensation.compensation import new_view, edit_view, \
remove_view IndexCompensationView, CompensationIdentifierGeneratorView
from compensation.views.compensation.log import CompensationLogView from compensation.views.compensation.log import CompensationLogView
urlpatterns = [ urlpatterns = [
# Main compensation # Main compensation
path("", index_view, name="index"), path("", IndexCompensationView.as_view(), name="index"),
path('new/id', new_id_view, name='new-id'), path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'),
path('new/<intervention_id>', new_view, name='new'), path('new/<intervention_id>', new_view, name='new'),
path('new', new_view, name='new'), path('new', new_view, name='new'),
path('<id>', detail_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', edit_view, name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'), path('<id>/remove', RemoveCompensationView.as_view(), name='remove'),
path('<id>/state/new', NewCompensationStateView.as_view(), name='new-state'), path('<id>/state/new', NewCompensationStateView.as_view(), name='new-state'),
path('<id>/state/<state_id>/edit', EditCompensationStateView.as_view(), name='state-edit'), path('<id>/state/<state_id>/edit', EditCompensationStateView.as_view(), name='state-edit'),
@ -43,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', report_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 index_view, new_view, new_id_view, edit_view, remove_view, \ from compensation.views.eco_account.detail import DetailEcoAccountView
detail_view from compensation.views.eco_account.eco_account import new_view, edit_view, \
IndexEcoAccountView, EcoAccountIdentifierGeneratorView
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 report_view 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,15 +30,15 @@ from compensation.views.eco_account.deduction import NewEcoAccountDeductionView,
app_name = "acc" app_name = "acc"
urlpatterns = [ urlpatterns = [
path("", index_view, name="index"), path("", IndexEcoAccountView.as_view(), name="index"),
path('new/', new_view, name='new'), path('new/', new_view, name='new'),
path('new/id', new_id_view, name='new-id'), path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'),
path('<id>', detail_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', report_view, name='report'), path('<id>/report', EcoAccountPublicReportView.as_view(), name='report'),
path('<id>/edit', edit_view, name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'), path('<id>/remove', RemoveEcoAccountView.as_view(), name='remove'),
path('<id>/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'), path('<id>/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'),
path('<id>/state/new', NewEcoAccountStateView.as_view(), name='new-state'), path('<id>/state/new', NewEcoAccountStateView.as_view(), name='new-state'),

View File

@ -8,10 +8,8 @@ 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.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum 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.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.forms.compensation import EditCompensationForm, NewCompensationForm from compensation.forms.compensation import EditCompensationForm, NewCompensationForm
@ -19,20 +17,17 @@ 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.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \ from konova.decorators import shared_access_required, default_group_required
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, \
RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \ IDENTIFIER_REPLACED, COMPENSATION_ADDED_TEMPLATE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.index import AbstractIndexView
@login_required class IndexCompensationView(AbstractIndexView):
@any_group_check def get(self, request, *args, **kwargs) -> HttpResponse:
def index_view(request: HttpRequest):
""" """
Renders the index view for compensation Renders the index view for compensation
@ -42,7 +37,6 @@ def index_view(request: HttpRequest):
Returns: Returns:
A rendered view A rendered view
""" """
template = "generic_index.html"
compensations = Compensation.objects.filter( 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
@ -58,8 +52,7 @@ def index_view(request: HttpRequest):
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"), TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)
@login_required @login_required
@default_group_required @default_group_required
@ -131,23 +124,8 @@ def new_view(request: HttpRequest, intervention_id: str = None):
return render(request, template, context) return render(request, template, context)
@login_required class CompensationIdentifierGeneratorView(AbstractIdentifierGeneratorView):
@default_group_required _MODEL = Compensation
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = Compensation()
identifier = tmp.generate_new_identifier()
while Compensation.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
@login_required @login_required
@ -213,105 +191,3 @@ def edit_view(request: HttpRequest, id: str):
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, template, context)
@login_required
@any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
template = "compensation/detail/compensation/view.html"
comp = get_object_or_404(
Compensation.objects.select_related(
"modified",
"created",
"geometry"
),
id=id,
deleted=None,
intervention__deleted=None,
)
geom_form = SimpleGeomForm(instance=comp)
parcels = comp.get_underlying_parcels()
_user = request.user
is_data_shared = comp.intervention.is_shared_with(_user)
# Order states according to surface
before_states = comp.before_states.all().prefetch_related("biotope_type").order_by("-surface")
after_states = comp.after_states.all().prefetch_related("biotope_type").order_by("-surface")
actions = comp.actions.all().prefetch_related("action_type")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = comp.get_surface_before_states()
sum_after_states = comp.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
request = comp.set_status_messages(request)
last_checked = comp.intervention.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
requesting_user_is_only_shared_user = comp.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = {
"obj": comp,
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"geom_form": geom_form,
"parcels": parcels,
"is_entry_shared": is_data_shared,
"actions": actions,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": comp.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}",
"has_finished_deadlines": comp.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required_modal
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request=request,
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
redirect_url=reverse("compensation:index"),
)

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

@ -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,20 +5,23 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.http import HttpRequest from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, render 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 django.utils.translation import gettext_lazy as _
from compensation.models import Compensation from compensation.models import Compensation
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import uuid_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.generators import generate_qr_code from konova.utils.qrcode import QrCode
from konova.views.report import AbstractPublicReportView
@uuid_required
def report_view(request: HttpRequest, id: str): class CompensationPublicReportView(AbstractPublicReportView):
_TEMPLATE = "compensation/report/compensation/report.html"
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" Renders the public report view """ Renders the public report view
Args: Args:
@ -28,10 +31,7 @@ def report_view(request: HttpRequest, id: str):
Returns: Returns:
""" """
# Reuse the compensation report template since compensations are structurally identical
template = "compensation/report/compensation/report.html"
comp = get_object_or_404(Compensation, id=id) comp = get_object_or_404(Compensation, id=id)
tab_title = _("Report {}").format(comp.identifier) tab_title = _("Report {}").format(comp.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data # If intervention is not recorded (yet or currently) we need to render another template without any data
if not comp.is_ready_for_publish(): if not comp.is_ready_for_publish():
@ -48,10 +48,14 @@ def report_view(request: HttpRequest, id: str):
) )
parcels = comp.get_underlying_parcels() parcels = comp.get_underlying_parcels()
qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,))) qrcode = QrCode(
qrcode_img = generate_qr_code(qrcode_url, 10) content=request.build_absolute_uri(reverse("compensation:report", args=(id,))),
qrcode_lanis_url = comp.get_LANIS_link() size=10
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) )
qrcode_lanis = QrCode(
content=comp.get_LANIS_link(),
size=7
)
# Order states by surface # Order states by surface
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type") before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
@ -61,12 +65,12 @@ def report_view(request: HttpRequest, id: str):
context = { context = {
"obj": comp, "obj": comp,
"qrcode": { "qrcode": {
"img": qrcode_img, "img": qrcode.get_img(),
"url": qrcode_url, "url": qrcode.get_content(),
}, },
"qrcode_lanis": { "qrcode_lanis": {
"img": qrcode_img_lanis, "img": qrcode_lanis.get_img(),
"url": qrcode_lanis_url, "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, "before_states": before_states,
@ -78,4 +82,4 @@ def report_view(request: HttpRequest, id: str):
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)

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

@ -7,28 +7,25 @@ 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.db.models import Sum from django.http import HttpRequest, HttpResponse
from django.http import HttpRequest, JsonResponse
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.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
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, any_group_check, login_required_modal, \ from konova.decorators import shared_access_required, default_group_required
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.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, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.index import AbstractIndexView
@login_required class IndexEcoAccountView(AbstractIndexView):
@any_group_check def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
def index_view(request: HttpRequest):
""" """
Renders the index view for eco accounts Renders the index view for eco accounts
@ -38,7 +35,6 @@ def index_view(request: HttpRequest):
Returns: Returns:
A rendered view A rendered view
""" """
template = "generic_index.html"
eco_accounts = EcoAccount.objects.filter( eco_accounts = EcoAccount.objects.filter(
deleted=None, deleted=None,
).order_by( ).order_by(
@ -53,7 +49,7 @@ def index_view(request: HttpRequest):
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"), TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)
@login_required @login_required
@ -111,25 +107,8 @@ def new_view(request: HttpRequest):
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, template, context)
class EcoAccountIdentifierGeneratorView(AbstractIdentifierGeneratorView):
@login_required _MODEL = EcoAccount
@default_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
while EcoAccount.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
@login_required @login_required
@default_group_required @default_group_required
@ -190,118 +169,3 @@ def edit_view(request: HttpRequest, id: str):
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, template, context)
@login_required
@any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
template = "compensation/detail/eco_account/view.html"
acc = get_object_or_404(
EcoAccount.objects.prefetch_related(
"deadlines",
).select_related(
'geometry',
'responsible',
),
id=id,
deleted=None,
)
geom_form = SimpleGeomForm(instance=acc)
parcels = acc.get_underlying_parcels()
_user = request.user
is_data_shared = acc.is_shared_with(_user)
# Order states according to surface
before_states = acc.before_states.order_by("-surface")
after_states = acc.after_states.order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = acc.get_surface_before_states()
sum_after_states = acc.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
# Calculate rest of available surface for deductions
available_total = acc.deductable_rest
available_relative = acc.get_deductable_rest_relative()
# Prefetch related data to decrease the amount of db connections
deductions = acc.deductions.filter(
intervention__deleted=None,
)
actions = acc.actions.all()
request = acc.set_status_messages(request)
requesting_user_is_only_shared_user = acc.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = {
"obj": acc,
"geom_form": geom_form,
"parcels": parcels,
"is_entry_shared": is_data_shared,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
"available": available_relative,
"available_total": available_total,
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": acc.get_LANIS_link(),
"deductions": deductions,
"actions": actions,
TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}",
"has_finished_deadlines": acc.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required_modal
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the eco account
Args:
request (HttpRequest): The incoming request
id (str): The account's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
# If the eco account has already been recorded OR there are already deductions, it can not be deleted by a regular
# default group user
if acc.recorded is not None or acc.deductions.exists():
user = request.user
if not user.in_group(ETS_GROUP):
messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED)
return redirect("compensation:acc:detail", id=id)
form = RemoveEcoAccountModalForm(request.POST or None, instance=acc, request=request)
return form.process_request(
request=request,
msg_success=_("Eco-account removed"),
redirect_url=reverse("compensation:acc:index"),
)

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,21 +5,23 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.http import HttpRequest from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, render 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 django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import uuid_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.generators import generate_qr_code from konova.utils.qrcode import QrCode
from konova.views.report import AbstractPublicReportView
@uuid_required class EcoAccountPublicReportView(AbstractPublicReportView):
def report_view(request: HttpRequest, id: str): _TEMPLATE = "compensation/report/eco_account/report.html"
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" Renders the public report view """ Renders the public report view
Args: Args:
@ -29,10 +31,7 @@ def report_view(request: HttpRequest, id: str):
Returns: Returns:
""" """
# Reuse the compensation report template since EcoAccounts are structurally identical
template = "compensation/report/eco_account/report.html"
acc = get_object_or_404(EcoAccount, id=id) acc = get_object_or_404(EcoAccount, id=id)
tab_title = _("Report {}").format(acc.identifier) tab_title = _("Report {}").format(acc.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data # If intervention is not recorded (yet or currently) we need to render another template without any data
if not acc.is_ready_for_publish(): if not acc.is_ready_for_publish():
@ -49,10 +48,14 @@ def report_view(request: HttpRequest, id: str):
) )
parcels = acc.get_underlying_parcels() parcels = acc.get_underlying_parcels()
qrcode_url = request.build_absolute_uri(reverse("compensation:acc:report", args=(id,))) qrcode = QrCode(
qrcode_img = generate_qr_code(qrcode_url, 10) content=request.build_absolute_uri(reverse("compensation:acc:report", args=(id,))),
qrcode_lanis_url = acc.get_LANIS_link() size=10
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) )
qrcode_lanis = QrCode(
content=acc.get_LANIS_link(),
size=7
)
# Order states by surface # Order states by surface
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent") before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent")
@ -68,12 +71,12 @@ def report_view(request: HttpRequest, id: str):
context = { context = {
"obj": acc, "obj": acc,
"qrcode": { "qrcode": {
"img": qrcode_img, "img": qrcode.get_img(),
"url": qrcode_url, "url": qrcode.get_content(),
}, },
"qrcode_lanis": { "qrcode_lanis": {
"img": qrcode_img_lanis, "img": qrcode_lanis.get_img(),
"url": qrcode_lanis_url, "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, "before_states": before_states,
@ -86,4 +89,4 @@ def report_view(request: HttpRequest, id: str):
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)

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>
{% 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 %}"> <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' %} {% fa5_icon 'share-alt' %}
</button> </button>
{% if is_ets_member %}
{% 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,16 +28,18 @@
{% fa5_icon 'bookmark' %} {% fa5_icon 'bookmark' %}
</button> </button>
{% endif %} {% endif %}
{% endif %}
{% if is_default_member %}
<a href="{% url 'ema:edit' obj.id %}" class="mr-2"> <a href="{% url 'ema:edit' obj.id %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}"> <button class="btn btn-default" title="{% trans 'Edit' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>
</a> </a>
{% endif %}
{% if is_default_member %}
<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 btn-modal mr-2" data-form-url="{% url 'ema:log' obj.id %}" title="{% trans 'Show log' %}">
{% fa5_icon 'history' %} {% fa5_icon 'history' %}
</button> </button>
{% endif %}
{% if is_ets_member %}
<button class="btn btn-default btn-modal" data-form-url="{% url 'ema:remove' obj.id %}" title="{% trans 'Delete' %}"> <button class="btn btn-default btn-modal" data-form-url="{% url 'ema:remove' obj.id %}" title="{% trans 'Delete' %}">
{% fa5_icon 'trash' %} {% fa5_icon 'trash' %}
</button> </button>

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,26 +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 index_view, new_view, new_id_view, detail_view, edit_view, remove_view from ema.views.ema import IndexEmaView, EmaIdentifierGeneratorView, EditEmaView, NewEmaView
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 report_view 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("", index_view, name="index"), path("", IndexEmaView.as_view(), name="index"),
path("new/", new_view, name="new"), path("new/", NewEmaView.as_view(), name="new"),
path("new/id", new_id_view, name="new-id"), path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"),
path("<id>", detail_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', edit_view, name='edit'), path('<id>/edit', EditEmaView.as_view(), name='edit'),
path('<id>/remove', remove_view, name='remove'), path('<id>/remove', RemoveEmaView.as_view(), name='remove'),
path('<id>/record', EmaRecordView.as_view(), name='record'), path('<id>/record', EmaRecordView.as_view(), name='record'),
path('<id>/report', report_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'),

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

@ -7,28 +7,28 @@ 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.db.models import Sum from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, JsonResponse 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.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.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal, \ from konova.decorators import shared_access_required, conservation_office_group_required
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \ from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.index import AbstractIndexView
@login_required class IndexEmaView(AbstractIndexView):
def index_view(request: HttpRequest): def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
""" Renders the index view for EMAs """ Renders the index view for EMAs
Args: Args:
@ -37,7 +37,6 @@ def index_view(request: HttpRequest):
Returns: Returns:
""" """
template = "generic_index.html"
emas = Ema.objects.filter( emas = Ema.objects.filter(
deleted=None, deleted=None,
).order_by( ).order_by(
@ -53,25 +52,51 @@ def index_view(request: HttpRequest):
TAB_TITLE_IDENTIFIER: _("EMAs - Overview"), TAB_TITLE_IDENTIFIER: _("EMAs - Overview"),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)
class NewEmaView(LoginRequiredMixin, View):
_TEMPLATE = "ema/form/view.html"
@login_required @method_decorator(conservation_office_group_required)
@conservation_office_group_required def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
def new_view(request: HttpRequest): """ GET endpoint
"""
Renders a view for a new eco account creation Renders form for new EMA
Args: Args:
request (HttpRequest): The incoming request 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: Returns:
""" """
template = "ema/form/view.html"
data_form = NewEmaForm(request.POST or None) data_form = NewEmaForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False) geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
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)
ema = data_form.save(request.user, geom_form) ema = data_form.save(request.user, geom_form)
@ -95,115 +120,79 @@ def new_view(request: HttpRequest):
request, request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
) )
return redirect("ema:detail", id=ema.id) return redirect("ema:detail", id=ema.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 = { context = {
"form": data_form, "form": data_form,
"geom_form": geom_form, "geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New EMA"), TAB_TITLE_IDENTIFIER: _("New EMA"),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)
class EmaIdentifierGeneratorView(AbstractIdentifierGeneratorView):
_MODEL = Ema
@login_required @method_decorator(conservation_office_group_required)
@conservation_office_group_required def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
def new_id_view(request: HttpRequest): return super().get(request, *args, **kwargs)
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls class EditEmaView(LoginRequiredMixin, View):
_TEMPLATE = "compensation/form/view.html"
""" @method_decorator(conservation_office_group_required)
tmp = Ema() @method_decorator(shared_access_required(Ema, "id"))
identifier = tmp.generate_new_identifier() def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
while Ema.objects.filter(identifier=identifier).exists(): """ GET endpoint
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
Renders form
@login_required
@uuid_required
def detail_view(request: HttpRequest, id: str):
""" Renders the detail view of an EMA
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The EMA id id (str): The ema identifier
*args ():
**kwargs ():
Returns: Returns:
""" """
template = "ema/detail/view.html" # Get object from db
ema = get_object_or_404(Ema, id=id, deleted=None) ema = get_object_or_404(Ema, id=id)
if ema.is_recorded:
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( messages.info(
request, request,
DO_NOT_FORGET_TO_SHARE 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 = { context = {
"obj": ema, "form": data_form,
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels, TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier),
"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 context = BaseContext(request, context).context
return render(request, template, 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
@login_required Process submitted forms
@conservation_office_group_required
@shared_access_required(Ema, "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
id (str): The id of the ema
*args ():
**kwargs ():
Returns: Returns:
""" """
template = "compensation/form/view.html"
# Get object from db # Get object from db
ema = get_object_or_404(Ema, id=id) ema = get_object_or_404(Ema, id=id)
if ema.is_recorded: if ema.is_recorded:
@ -216,7 +205,6 @@ def edit_view(request: HttpRequest, id: str):
# Create forms, initialize with values from db/from POST request # Create forms, initialize with values from db/from POST request
data_form = EditEmaForm(request.POST or None, instance=ema) data_form = EditEmaForm(request.POST or None, instance=ema)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema) geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid(): 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
ema = data_form.save(request.user, geom_form) ema = data_form.save(request.user, geom_form)
@ -226,48 +214,19 @@ 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("ema:detail", id=ema.id) return redirect("ema:detail", id=ema.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 = { context = {
"form": data_form, "form": data_form,
"geom_form": geom_form, "geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier), TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)
@login_required_modal
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = RemoveModalForm(request.POST or None, instance=ema, request=request)
return form.process_request(
request=request,
msg_success=_("EMA removed"),
redirect_url=reverse("ema:index"),
)

View File

@ -18,7 +18,6 @@ class EmaLogView(AbstractLogView):
@method_decorator(login_required_modal) @method_decorator(login_required_modal)
@method_decorator(login_required) @method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id")) @method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
return super().dispatch(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,20 +5,23 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.http import HttpRequest from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, render 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 django.utils.translation import gettext_lazy as _
from ema.models import Ema from ema.models import Ema
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import uuid_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.generators import generate_qr_code from konova.utils.qrcode import QrCode
from konova.views.report import AbstractPublicReportView
@uuid_required
def report_view(request:HttpRequest, id: str): class EmaPublicReportView(AbstractPublicReportView):
_TEMPLATE = "ema/report/report.html"
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" Renders the public report view """ Renders the public report view
Args: Args:
@ -28,10 +31,7 @@ def report_view(request:HttpRequest, id: str):
Returns: Returns:
""" """
# Reuse the compensation report template since EMAs are structurally identical
template = "ema/report/report.html"
ema = get_object_or_404(Ema, id=id) ema = get_object_or_404(Ema, id=id)
tab_title = _("Report {}").format(ema.identifier) tab_title = _("Report {}").format(ema.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data # If intervention is not recorded (yet or currently) we need to render another template without any data
if not ema.is_ready_for_publish(): if not ema.is_ready_for_publish():
@ -48,10 +48,14 @@ def report_view(request:HttpRequest, id: str):
) )
parcels = ema.get_underlying_parcels() parcels = ema.get_underlying_parcels()
qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,))) qrcode = QrCode(
qrcode_img = generate_qr_code(qrcode_url, 10) content=request.build_absolute_uri(reverse("ema:report", args=(id,))),
qrcode_lanis_url = ema.get_LANIS_link() size=10
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) )
qrcode_lanis = QrCode(
content=ema.get_LANIS_link(),
size=7
)
# Order states by surface # Order states by surface
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type") before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type")
@ -61,12 +65,12 @@ def report_view(request:HttpRequest, id: str):
context = { context = {
"obj": ema, "obj": ema,
"qrcode": { "qrcode": {
"img": qrcode_img, "img": qrcode.get_img(),
"url": qrcode_url "url": qrcode.get_content(),
}, },
"qrcode_lanis": { "qrcode_lanis": {
"img": qrcode_img_lanis, "img": qrcode_lanis.get_img(),
"url": qrcode_lanis_url "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, "before_states": before_states,
@ -78,4 +82,4 @@ def report_view(request:HttpRequest, id: str):
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)

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

@ -8,39 +8,42 @@ Created on: 30.11.20
from django.urls import path from django.urls import path
from intervention.autocomplete.intervention import InterventionAutocomplete from intervention.autocomplete.intervention import InterventionAutocomplete
from intervention.views.check import check_view from intervention.views.check import InterventionCheckView
from intervention.views.compensation import remove_compensation_view from intervention.views.compensation import RemoveCompensationFromInterventionView
from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \ from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \
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 index_view, new_view, new_id_view, detail_view, edit_view, remove_view from intervention.views.intervention import IndexInterventionView, InterventionIdentifierGeneratorView, \
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 report_view from intervention.views.report import InterventionPublicReportView
from intervention.views.resubmission import InterventionResubmissionView from intervention.views.resubmission import InterventionResubmissionView
from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \ from intervention.views.revocation import NewInterventionRevocationView, GetInterventionRevocationView, \
get_revocation_view 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("", index_view, name="index"), path("", IndexInterventionView.as_view(), name="index"),
path('new/', new_view, name='new'), path('new/', NewInterventionView.as_view(), name='new'),
path('new/id', new_id_view, name='new-id'), path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'),
path('<id>', detail_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', edit_view, name='edit'), path('<id>/edit', EditInterventionView.as_view(), name='edit'),
path('<id>/remove', remove_view, name='remove'), path('<id>/remove', RemoveInterventionView.as_view(), name='remove'),
path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'), path('<id>/share/<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', check_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', report_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
path('<id>/compensation/<comp_id>/remove', remove_compensation_view, name='remove-compensation'), path('<id>/compensation/<comp_id>/remove', RemoveCompensationFromInterventionView.as_view(), name='remove-compensation'),
# Documents # Documents
path('<id>/document/new/', NewInterventionDocumentView.as_view(), name='new-doc'), path('<id>/document/new/', NewInterventionDocumentView.as_view(), name='new-doc'),
@ -54,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', new_revocation_view, name='new-revocation'), path('<id>/revocation/new', NewInterventionRevocationView.as_view(), name='new-revocation'),
path('<id>/revocation/<revocation_id>/edit', edit_revocation_view, name='edit-revocation'), path('<id>/revocation/<revocation_id>/edit', EditInterventionRevocationView.as_view(), name='edit-revocation'),
path('<id>/revocation/<revocation_id>/remove', remove_revocation_view, name='remove-revocation'), path('<id>/revocation/<revocation_id>/remove', RemoveInterventionRevocationView.as_view(), name='remove-revocation'),
path('revocation/<doc_id>', get_revocation_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

@ -5,21 +5,21 @@ 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.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404 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.decorators import registration_office_group_required, shared_access_required from konova.decorators import registration_office_group_required, shared_access_required
from konova.utils.message_templates import INTERVENTION_INVALID from konova.utils.message_templates import INTERVENTION_INVALID
class InterventionCheckView(LoginRequiredMixin, View):
@login_required def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
@registration_office_group_required
@shared_access_required(Intervention, "id")
def check_view(request: HttpRequest, id: str):
""" Renders check form for an intervention """ Renders check form for an intervention
Args: Args:
@ -37,3 +37,12 @@ def check_view(request: HttpRequest, id: str):
msg_error=INTERVENTION_INVALID 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

@ -5,22 +5,23 @@ 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.contrib.auth.mixins import LoginRequiredMixin
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest, Http404 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 intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import shared_access_required, login_required_modal from konova.decorators import shared_access_required
from konova.forms.modals import RemoveModalForm from konova.forms.modals import RemoveModalForm
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
@login_required_modal class RemoveCompensationFromInterventionView(LoginRequiredMixin, View):
@login_required
@shared_access_required(Intervention, "id") def __process_request(self, request: HttpRequest, id: str, comp_id: str, *args, **kwargs) -> HttpResponse:
def remove_compensation_view(request: HttpRequest, id: str, comp_id: str):
""" Renders a modal view for removing the compensation """ Renders a modal view for removing the compensation
Args: Args:
@ -44,3 +45,10 @@ def remove_compensation_view(request: HttpRequest, id: str, comp_id: str):
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data", 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

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

@ -7,29 +7,29 @@ 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.http import JsonResponse, HttpRequest from django.contrib.auth.mixins import LoginRequiredMixin
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.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 intervention.forms.intervention import EditInterventionForm, NewInterventionForm from intervention.forms.intervention import EditInterventionForm, NewInterventionForm
from intervention.models import Intervention from intervention.models import Intervention
from intervention.tables import InterventionTable from intervention.tables import InterventionTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import default_group_required, shared_access_required, any_group_check, login_required_modal, \ from konova.decorators import default_group_required, shared_access_required
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.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, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \ CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, \
GEOMETRIES_IGNORED_TEMPLATE GEOMETRIES_IGNORED_TEMPLATE
from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.index import AbstractIndexView
@login_required class IndexInterventionView(AbstractIndexView):
@any_group_check def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
def index_view(request: HttpRequest):
""" """
Renders the index view for Interventions Renders the index view for Interventions
@ -39,8 +39,6 @@ def index_view(request: HttpRequest):
Returns: Returns:
A rendered view A rendered view
""" """
template = "generic_index.html"
# Filtering by user access is performed in table filter inside InterventionTableFilter class # Filtering by user access is performed in table filter inside InterventionTableFilter class
interventions = Intervention.objects.filter( interventions = Intervention.objects.filter(
deleted=None, # not deleted deleted=None, # not deleted
@ -58,12 +56,37 @@ def index_view(request: HttpRequest):
TAB_TITLE_IDENTIFIER: _("Interventions - Overview"), TAB_TITLE_IDENTIFIER: _("Interventions - Overview"),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)
@login_required class NewInterventionView(LoginRequiredMixin, View):
@default_group_required _TEMPLATE = "intervention/form/view.html"
def new_view(request: HttpRequest):
@method_decorator(default_group_required)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""
Renders a view for a new intervention creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
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 Renders a view for a new intervention creation
@ -73,10 +96,9 @@ def new_view(request: HttpRequest):
Returns: Returns:
""" """
template = "intervention/form/view.html"
data_form = NewInterventionForm(request.POST or None) data_form = NewInterventionForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False) geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
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)
intervention = data_form.save(request.user, geom_form) intervention = data_form.save(request.user, geom_form)
@ -88,6 +110,7 @@ def new_view(request: HttpRequest):
intervention.identifier intervention.identifier
) )
) )
messages.success(request, _("Intervention {} added").format(intervention.identifier)) messages.success(request, _("Intervention {} added").format(intervention.identifier))
if geom_form.has_geometry_simplified(): if geom_form.has_geometry_simplified():
messages.info( messages.info(
@ -101,129 +124,74 @@ def new_view(request: HttpRequest):
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: 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 = { context = {
"form": data_form, "form": data_form,
"geom_form": geom_form, "geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New intervention"), TAB_TITLE_IDENTIFIER: _("New intervention"),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)
@login_required class InterventionIdentifierGeneratorView(AbstractIdentifierGeneratorView):
@default_group_required _MODEL = Intervention
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp_intervention = Intervention()
identifier = tmp_intervention.generate_new_identifier()
while Intervention.objects.filter(identifier=identifier).exists():
identifier = tmp_intervention.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
}
)
@login_required class EditInterventionView(LoginRequiredMixin, View):
@any_group_check _TEMPLATE = "intervention/form/view.html"
@uuid_required
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for viewing an intervention's data
Args: @method_decorator(default_group_required)
request (HttpRequest): The incoming request @method_decorator(shared_access_required(Intervention, "id"))
id (str): The intervention's id def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
Returns:
"""
template = "intervention/detail/view.html"
# Fetch data, filter out deleted related data
intervention = get_object_or_404(
Intervention.objects.select_related(
"geometry",
"legal",
"responsible",
).prefetch_related(
"legal__revocations",
),
id=id,
deleted=None
)
compensations = intervention.compensations.filter(
deleted=None,
)
_user = request.user
is_data_shared = intervention.is_shared_with(user=_user)
geom_form = SimpleGeomForm(
instance=intervention,
)
last_checked = intervention.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
last_checked.get_timestamp_str_formatted(),
last_checked.user
)
has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists()
requesting_user_is_only_shared_user = intervention.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = {
"obj": intervention,
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"compensations": compensations,
"is_entry_shared": is_data_shared,
"geom_form": geom_form,
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": intervention.get_LANIS_link(),
"has_payment_without_document": has_payment_without_document,
TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}",
}
request = intervention.set_status_messages(request)
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
@shared_access_required(Intervention, "id")
def edit_view(request: HttpRequest, id: str):
""" """
Renders a view for editing interventions Renders a view for editing interventions
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The intervention identifier
Returns: 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:
""" """
template = "intervention/form/view.html"
# Get object from db # Get object from db
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
if intervention.is_recorded: if intervention.is_recorded:
@ -236,7 +204,6 @@ def edit_view(request: HttpRequest, id: str):
# Create forms, initialize with values from db/from POST request # Create forms, initialize with values from db/from POST request
data_form = EditInterventionForm(request.POST or None, instance=intervention) data_form = EditInterventionForm(request.POST or None, instance=intervention)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention) geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid(): 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
@ -250,48 +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:
messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = { context = {
"form": data_form, "form": data_form,
"geom_form": geom_form, "geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier), TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)
@login_required_modal
@login_required
@default_group_required
@shared_access_required(Intervention, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a remove view for this intervention
Args:
request (HttpRequest): The incoming request
id (str): The uuid id as string
Returns:
"""
obj = Intervention.objects.get(id=id)
identifier = obj.identifier
form = RemoveModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
_("{} removed").format(identifier),
redirect_url=reverse("intervention:index")
)

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,21 +5,23 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.http import HttpRequest from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, render 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 django.utils.translation import gettext_lazy as _
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import uuid_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.generators import generate_qr_code from konova.utils.qrcode import QrCode
from konova.views.report import AbstractPublicReportView
@uuid_required class InterventionPublicReportView(AbstractPublicReportView):
def report_view(request: HttpRequest, id: str): _TEMPLATE = "intervention/report/report.html"
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" Renders the public report view """ Renders the public report view
Args: Args:
@ -29,7 +31,6 @@ def report_view(request: HttpRequest, id: str):
Returns: Returns:
""" """
template = "intervention/report/report.html"
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
tab_title = _("Report {}").format(intervention.identifier) tab_title = _("Report {}").format(intervention.identifier)
@ -51,21 +52,26 @@ def report_view(request: HttpRequest, id: str):
distinct_deductions = intervention.deductions.all().distinct( distinct_deductions = intervention.deductions.all().distinct(
"account" "account"
) )
qrcode_url = request.build_absolute_uri(reverse("intervention:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10) qrcode = QrCode(
qrcode_lanis_url = intervention.get_LANIS_link() content=request.build_absolute_uri(reverse("intervention:report", args=(id,))),
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) size=10
)
qrcode_lanis = QrCode(
content=intervention.get_LANIS_link(),
size=7
)
context = { context = {
"obj": intervention, "obj": intervention,
"deductions": distinct_deductions, "deductions": distinct_deductions,
"qrcode": { "qrcode": {
"img": qrcode_img, "img": qrcode.get_img(),
"url": qrcode_url, "url": qrcode.get_content(),
}, },
"qrcode_lanis": { "qrcode_lanis": {
"img": qrcode_img_lanis, "img": qrcode_lanis.get_img(),
"url": qrcode_lanis_url, "url": qrcode_lanis.get_content(),
}, },
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels, "parcels": parcels,
@ -73,4 +79,4 @@ def report_view(request: HttpRequest, id: str):
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, self._TEMPLATE, context)

View File

@ -6,10 +6,12 @@ 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.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.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
@ -19,10 +21,8 @@ from konova.utils.documents import get_document
from konova.utils.message_templates import REVOCATION_ADDED, DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED from konova.utils.message_templates import REVOCATION_ADDED, DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED
@login_required class NewInterventionRevocationView(LoginRequiredMixin, View):
@default_group_required def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
@shared_access_required(Intervention, "id")
def new_revocation_view(request: HttpRequest, id: str):
""" Renders sharing form for an intervention """ Renders sharing form for an intervention
Args: Args:
@ -33,17 +33,28 @@ def new_revocation_view(request: HttpRequest, id: str):
""" """
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, request=request) form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention,
request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=REVOCATION_ADDED, msg_success=REVOCATION_ADDED,
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" 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)
@login_required @method_decorator(default_group_required)
@default_group_required @method_decorator(shared_access_required(Intervention, "id"))
def get_revocation_view(request: HttpRequest, doc_id: str): def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, *args, **kwargs)
class GetInterventionRevocationView(LoginRequiredMixin, View):
@method_decorator(default_group_required)
def get(self, request: HttpRequest, doc_id: str, *args, **kwargs) -> HttpResponse:
""" Returns the revocation document as downloadable file """ Returns the revocation document as downloadable file
Wraps the generic document fetcher function from konova.utils. Wraps the generic document fetcher function from konova.utils.
@ -66,10 +77,8 @@ def get_revocation_view(request: HttpRequest, doc_id: str):
return get_document(doc) return get_document(doc)
@login_required class EditInterventionRevocationView(LoginRequiredMixin, View):
@default_group_required def __process_request(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
@shared_access_required(Intervention, "id")
def edit_revocation_view(request: HttpRequest, id: str, revocation_id: str):
""" Renders a edit view for a revocation """ Renders a edit view for a revocation
Args: Args:
@ -83,19 +92,27 @@ def edit_revocation_view(request: HttpRequest, id: str, revocation_id: str):
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
revocation = get_object_or_404(Revocation, id=revocation_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) form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention,
revocation=revocation, request=request)
return form.process_request( return form.process_request(
request, request,
REVOCATION_EDITED, REVOCATION_EDITED,
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" 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)
@login_required_modal @method_decorator(default_group_required)
@login_required @method_decorator(shared_access_required(Intervention, "id"))
@default_group_required def post(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
@shared_access_required(Intervention, "id") return self.__process_request(request, id, revocation_id, *args, **kwargs)
def remove_revocation_view(request: HttpRequest, id: str, revocation_id: str):
class RemoveInterventionRevocationView(LoginRequiredMixin, View):
def __process_request(self, request, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
""" Renders a remove view for a revocation """ Renders a remove view for a revocation
Args: Args:
@ -109,10 +126,20 @@ def remove_revocation_view(request: HttpRequest, id: str, revocation_id: str):
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
revocation = get_object_or_404(Revocation, id=revocation_id) revocation = get_object_or_404(Revocation, id=revocation_id)
form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation, request=request) form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation,
request=request)
return form.process_request( return form.process_request(
request, request,
REVOCATION_REMOVED, REVOCATION_REMOVED,
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" 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

@ -42,23 +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 generate_qr_code(content: str, size: int = 20) -> str: def __init__(self, model):
""" Generates a qr code from given content from konova.models import BaseObject
if not issubclass(model, BaseObject):
raise AssertionError("Model must be a subclass of BaseObject!")
Args: self._MODEL = model
content (str): The content for the qr code
size (int): The image size def generate_id(self) -> str:
""" Generates a unique identifier
Returns: Returns:
qrcode_svg (str): The qr code as svg
""" """
qrcode_factory = qrcode.image.svg.SvgImage unpersisted_object = self._MODEL()
qrcode_img = qrcode.make( identifier = unpersisted_object.generate_new_identifier()
content, while self._MODEL.objects.filter(identifier=identifier).exists():
image_factory=qrcode_factory, identifier = unpersisted_object.generate_new_identifier()
box_size=size return identifier
)
stream = BytesIO()
qrcode_img.save(stream)
return stream.getvalue().decode()

47
konova/utils/qrcode.py Normal file
View File

@ -0,0 +1,47 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from io import BytesIO
import qrcode
import qrcode.image.svg as svg
class QrCode:
""" A wrapping class for creating a qr code with content
"""
_content = None
_img = None
def __init__(self, content: str, size: int):
self._content = content
self._img = self._generate_qr_code(content, size)
def _generate_qr_code(self, content: str, size: int = 20) -> str:
""" Generates a qr code from given content
Args:
content (str): The content for the qr code
size (int): The image size
Returns:
qrcode_svg (str): The qr code as svg
"""
img_factory = svg.SvgImage
qrcode_img = qrcode.make(
content,
image_factory=img_factory,
box_size=size
)
stream = BytesIO()
qrcode_img.save(stream)
return stream.getvalue().decode()
def get_img(self):
return self._img
def get_content(self):
return self._content

25
konova/views/detail.py Normal file
View File

@ -0,0 +1,25 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from abc import ABC
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse
from django.utils.decorators import method_decorator
from django.views import View
from konova.decorators import uuid_required, any_group_check
class AbstractDetailView(LoginRequiredMixin, View, ABC):
_TEMPLATE = None
@method_decorator(uuid_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
@method_decorator(any_group_check)
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
raise NotImplementedError()

View File

@ -0,0 +1,28 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from abc import ABC
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, JsonResponse
from django.utils.decorators import method_decorator
from django.views import View
from konova.decorators import default_group_required
from konova.utils.generators import IdentifierGenerator
class AbstractIdentifierGeneratorView(LoginRequiredMixin, View, ABC):
_MODEL = None
@method_decorator(default_group_required)
def get(self, request: HttpRequest, *args, **kwargs):
generator = IdentifierGenerator(model=self._MODEL)
identifier = generator.generate_id()
return JsonResponse(
data={
"gen_data": identifier
}
)

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

@ -0,0 +1,21 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from abc import ABC
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse
from django.utils.decorators import method_decorator
from django.views import View
from konova.decorators import any_group_check
class AbstractIndexView(LoginRequiredMixin, View, ABC):
_TEMPLATE = "generic_index.html"
@method_decorator(any_group_check)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
raise NotImplementedError()

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

@ -0,0 +1,64 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from abc import ABC
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse
from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
from django.utils.translation import gettext_lazy as _
from konova.decorators import default_group_required
from konova.forms.modals import RemoveModalForm
class AbstractRemoveView(LoginRequiredMixin, View, ABC):
_MODEL = None
_REDIRECT_URL = None
_FORM = RemoveModalForm
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
@method_decorator(default_group_required)
def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
obj = self._MODEL.objects.get(id=id)
identifier = obj.identifier
form = self._FORM(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
_("{} removed").format(identifier),
redirect_url=reverse(self._REDIRECT_URL)
)
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" GET endpoint for removing via modal form
Due to the legacy logic of the form (which processes get and post requests directly), we simply need to pipe
the request from GET and POST endpoints directly into the same method.
Args:
request (HttpRequest): The incoming request
id (str): The uuid id as string
Returns:
"""
return self.__process_request(request, id, *args, **kwargs)
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" POST endpoint for removing via modal form
Due to the legacy logic of the form (which processes get and post requests directly), we simply need to pipe
the request from GET and POST endpoints directly into the same method.
Args:
request (HttpRequest): The incoming request
id (str): The uuid id as string
Returns:
"""
return self.__process_request(request, id, *args, **kwargs)

24
konova/views/report.py Normal file
View File

@ -0,0 +1,24 @@
"""
Author: Michel Peltriaux
Created on: 14.12.25
"""
from abc import abstractmethod, ABC
from django.http import HttpRequest, HttpResponse
from django.utils.decorators import method_decorator
from django.views import View
from konova.decorators import uuid_required
class AbstractPublicReportView(View, ABC):
_TEMPLATE = None
@method_decorator(uuid_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
@abstractmethod
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
raise NotImplementedError()

Binary file not shown.

View File

@ -45,7 +45,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-15 09:11+0200\n" "POT-Creation-Date: 2025-12-14 17:23+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -448,7 +448,7 @@ msgid "Select the intervention for which this compensation compensates"
msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist" msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist"
#: compensation/forms/compensation.py:114 #: compensation/forms/compensation.py:114
#: compensation/views/compensation/compensation.py:120 #: compensation/views/compensation/compensation.py:121
msgid "New compensation" msgid "New compensation"
msgstr "Neue Kompensation" msgstr "Neue Kompensation"
@ -456,38 +456,38 @@ msgstr "Neue Kompensation"
msgid "Edit compensation" msgid "Edit compensation"
msgstr "Bearbeite Kompensation" msgstr "Bearbeite Kompensation"
#: compensation/forms/eco_account.py:31 compensation/utils/quality.py:97 #: compensation/forms/eco_account.py:32 compensation/utils/quality.py:97
msgid "Available Surface" msgid "Available Surface"
msgstr "Verfügbare Fläche" msgstr "Verfügbare Fläche"
#: compensation/forms/eco_account.py:34 #: compensation/forms/eco_account.py:35
msgid "The amount that can be used for deductions" msgid "The amount that can be used for deductions"
msgstr "Die für Abbuchungen zur Verfügung stehende Menge" msgstr "Die für Abbuchungen zur Verfügung stehende Menge"
#: compensation/forms/eco_account.py:43 #: compensation/forms/eco_account.py:44
#: compensation/templates/compensation/detail/eco_account/view.html:67 #: compensation/templates/compensation/detail/eco_account/view.html:67
#: compensation/utils/quality.py:84 #: compensation/utils/quality.py:84
msgid "Agreement date" msgid "Agreement date"
msgstr "Vereinbarungsdatum" msgstr "Vereinbarungsdatum"
#: compensation/forms/eco_account.py:45 #: compensation/forms/eco_account.py:46
msgid "When did the parties agree on this?" msgid "When did the parties agree on this?"
msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?" msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?"
#: compensation/forms/eco_account.py:72 #: compensation/forms/eco_account.py:73
#: compensation/views/eco_account/eco_account.py:101 #: compensation/views/eco_account/eco_account.py:105
msgid "New Eco-Account" msgid "New Eco-Account"
msgstr "Neues Ökokonto" msgstr "Neues Ökokonto"
#: compensation/forms/eco_account.py:81 #: compensation/forms/eco_account.py:82
msgid "Eco-Account XY; Location ABC" msgid "Eco-Account XY; Location ABC"
msgstr "Ökokonto XY; Flur ABC" msgstr "Ökokonto XY; Flur ABC"
#: compensation/forms/eco_account.py:147 #: compensation/forms/eco_account.py:148
msgid "Edit Eco-Account" msgid "Edit Eco-Account"
msgstr "Ökokonto bearbeiten" msgstr "Ökokonto bearbeiten"
#: compensation/forms/eco_account.py:183 #: compensation/forms/eco_account.py:184
msgid "" msgid ""
"{}m² have been deducted from this eco account so far. The given value of {} " "{}m² have been deducted from this eco account so far. The given value of {} "
"would be too low." "would be too low."
@ -495,12 +495,16 @@ msgstr ""
"{}n² wurden bereits von diesem Ökokonto abgebucht. Der eingegebene Wert von " "{}n² wurden bereits von diesem Ökokonto abgebucht. Der eingegebene Wert von "
"{} wäre daher zu klein." "{} wäre daher zu klein."
#: compensation/forms/eco_account.py:247 #: compensation/forms/eco_account.py:248
msgid "The account can not be removed, since there are still deductions." msgid "The account can not be removed, since there are still deductions."
msgstr "" msgstr ""
"Das Ökokonto kann nicht entfernt werden, da hierzu noch Abbuchungen " "Das Ökokonto kann nicht entfernt werden, da hierzu noch Abbuchungen "
"vorliegen." "vorliegen."
#: compensation/forms/eco_account.py:257
msgid "Please contact the responsible conservation office to find a solution!"
msgstr "Kontaktieren Sie die zuständige Naturschutzbehörde um eine Lösung zu finden!"
#: compensation/forms/mixins.py:37 #: compensation/forms/mixins.py:37
#: compensation/templates/compensation/detail/eco_account/view.html:63 #: compensation/templates/compensation/detail/eco_account/view.html:63
#: compensation/templates/compensation/report/eco_account/report.html:20 #: compensation/templates/compensation/report/eco_account/report.html:20
@ -1288,44 +1292,40 @@ msgstr ""
msgid "Responsible data" msgid "Responsible data"
msgstr "Daten zu den verantwortlichen Stellen" msgstr "Daten zu den verantwortlichen Stellen"
#: compensation/views/compensation/compensation.py:58 #: compensation/views/compensation/compensation.py:52
msgid "Compensations - Overview" msgid "Compensations - Overview"
msgstr "Kompensationen - Übersicht" msgstr "Kompensationen - Übersicht"
#: compensation/views/compensation/compensation.py:181 #: compensation/views/compensation/compensation.py:167
#: konova/utils/message_templates.py:40 #: konova/utils/message_templates.py:40
msgid "Compensation {} edited" msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet" msgstr "Kompensation {} bearbeitet"
#: compensation/views/compensation/compensation.py:196 #: compensation/views/compensation/compensation.py:190
#: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:238 #: compensation/views/eco_account/eco_account.py:168 ema/views/ema.py:173
#: intervention/views/intervention.py:253 #: intervention/views/intervention.py:175
msgid "Edit {}" msgid "Edit {}"
msgstr "Bearbeite {}" msgstr "Bearbeite {}"
#: compensation/views/compensation/report.py:35 #: compensation/views/compensation/report.py:35
#: compensation/views/eco_account/report.py:36 ema/views/report.py:35 #: compensation/views/eco_account/report.py:35 ema/views/report.py:35
#: intervention/views/report.py:35 #: intervention/views/report.py:36
msgid "Report {}" msgid "Report {}"
msgstr "Bericht {}" msgstr "Bericht {}"
#: compensation/views/eco_account/eco_account.py:53 #: compensation/views/eco_account/eco_account.py:49
msgid "Eco-account - Overview" msgid "Eco-account - Overview"
msgstr "Ökokonten - Übersicht" msgstr "Ökokonten - Übersicht"
#: compensation/views/eco_account/eco_account.py:86 #: compensation/views/eco_account/eco_account.py:82
msgid "Eco-Account {} added" msgid "Eco-Account {} added"
msgstr "Ökokonto {} hinzugefügt" msgstr "Ökokonto {} hinzugefügt"
#: compensation/views/eco_account/eco_account.py:158 #: compensation/views/eco_account/eco_account.py:145
msgid "Eco-Account {} edited" msgid "Eco-Account {} edited"
msgstr "Ökokonto {} bearbeitet" msgstr "Ökokonto {} bearbeitet"
#: compensation/views/eco_account/eco_account.py:288 #: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:107
msgid "Eco-account removed"
msgstr "Ökokonto entfernt"
#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:108
msgid "New EMA" msgid "New EMA"
msgstr "Neue EMA hinzufügen" msgstr "Neue EMA hinzufügen"
@ -1353,22 +1353,18 @@ msgstr ""
msgid "Payment funded compensation" msgid "Payment funded compensation"
msgstr "Ersatzzahlungsmaßnahme" msgstr "Ersatzzahlungsmaßnahme"
#: ema/views/ema.py:53 #: ema/views/ema.py:52
msgid "EMAs - Overview" msgid "EMAs - Overview"
msgstr "EMAs - Übersicht" msgstr "EMAs - Übersicht"
#: ema/views/ema.py:86 #: ema/views/ema.py:85
msgid "EMA {} added" msgid "EMA {} added"
msgstr "EMA {} hinzugefügt" msgstr "EMA {} hinzugefügt"
#: ema/views/ema.py:223 #: ema/views/ema.py:150
msgid "EMA {} edited" msgid "EMA {} edited"
msgstr "EMA {} bearbeitet" msgstr "EMA {} bearbeitet"
#: ema/views/ema.py:262
msgid "EMA removed"
msgstr "EMA entfernt"
#: intervention/forms/intervention.py:49 #: intervention/forms/intervention.py:49
msgid "Construction XY; Location ABC" msgid "Construction XY; Location ABC"
msgstr "Bauvorhaben XY; Flur ABC" msgstr "Bauvorhaben XY; Flur ABC"
@ -1429,7 +1425,7 @@ msgstr "Datum Bestandskraft bzw. Rechtskraft"
#: intervention/forms/intervention.py:216 #: intervention/forms/intervention.py:216
#: intervention/tests/unit/test_forms.py:36 #: intervention/tests/unit/test_forms.py:36
#: intervention/views/intervention.py:105 #: intervention/views/intervention.py:109
msgid "New intervention" msgid "New intervention"
msgstr "Neuer Eingriff" msgstr "Neuer Eingriff"
@ -1665,22 +1661,18 @@ msgstr ""
msgid "Check performed" msgid "Check performed"
msgstr "Prüfung durchgeführt" msgstr "Prüfung durchgeführt"
#: intervention/views/intervention.py:57 #: intervention/views/intervention.py:53
msgid "Interventions - Overview" msgid "Interventions - Overview"
msgstr "Eingriffe - Übersicht" msgstr "Eingriffe - Übersicht"
#: intervention/views/intervention.py:90 #: intervention/views/intervention.py:86
msgid "Intervention {} added" msgid "Intervention {} added"
msgstr "Eingriff {} hinzugefügt" msgstr "Eingriff {} hinzugefügt"
#: intervention/views/intervention.py:236 #: intervention/views/intervention.py:150
msgid "Intervention {} edited" msgid "Intervention {} edited"
msgstr "Eingriff {} bearbeitet" msgstr "Eingriff {} bearbeitet"
#: intervention/views/intervention.py:278
msgid "{} removed"
msgstr "{} entfernt"
#: konova/decorators.py:32 #: konova/decorators.py:32
msgid "You need to be staff to perform this action!" msgid "You need to be staff to perform this action!"
msgstr "Hierfür müssen Sie Mitarbeiter sein!" msgstr "Hierfür müssen Sie Mitarbeiter sein!"
@ -1810,7 +1802,7 @@ msgstr "Nicht editierbar"
msgid "Geometry" msgid "Geometry"
msgstr "Geometrie" msgstr "Geometrie"
#: konova/forms/geometry_form.py:100 #: konova/forms/geometry_form.py:105
msgid "Only surfaces allowed. Points or lines must be buffered." msgid "Only surfaces allowed. Points or lines must be buffered."
msgstr "" msgstr ""
"Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden." "Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden."
@ -2268,8 +2260,9 @@ msgid ""
"too small to be valid). These parts have been removed. Please check the " "too small to be valid). These parts have been removed. Please check the "
"stored geometry." "stored geometry."
msgstr "" msgstr ""
"Die Geometrie enthielt {} invalide Bestandteile (z.B. unaussagekräftige Kleinstflächen)." "Die Geometrie enthielt {} invalide Bestandteile (z.B. unaussagekräftige "
"Diese Bestandteile wurden automatisch entfernt. Bitte überprüfen Sie die angepasste Geometrie." "Kleinstflächen).Diese Bestandteile wurden automatisch entfernt. Bitte "
"überprüfen Sie die angepasste Geometrie."
#: konova/utils/message_templates.py:89 #: konova/utils/message_templates.py:89
msgid "This intervention has {} revocations" msgid "This intervention has {} revocations"
@ -2330,6 +2323,10 @@ msgstr "{} verzeichnet"
msgid "Errors found:" msgid "Errors found:"
msgstr "Fehler gefunden:" msgstr "Fehler gefunden:"
#: konova/views/remove.py:35
msgid "{} removed"
msgstr "{} entfernt"
#: konova/views/resubmission.py:39 #: konova/views/resubmission.py:39
msgid "Resubmission set" msgid "Resubmission set"
msgstr "Wiedervorlage gesetzt" msgstr "Wiedervorlage gesetzt"