diff --git a/analysis/urls.py b/analysis/urls.py index 71eb6665..c51d70f0 100644 --- a/analysis/urls.py +++ b/analysis/urls.py @@ -10,6 +10,6 @@ from analysis.views import * app_name = "analysis" urlpatterns = [ - path("reports/", index_reports_view, name="reports"), - path("reports/", detail_report_view, name="report-detail"), + path("reports/", ReportIndexView.as_view(), name="reports"), + path("reports/", ReportDetailView.as_view(), name="report-detail"), ] \ No newline at end of file diff --git a/analysis/views.py b/analysis/views.py index 6da4a586..525fdd36 100644 --- a/analysis/views.py +++ b/analysis/views.py @@ -1,8 +1,12 @@ 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, HttpResponse from django.shortcuts import render, redirect, get_object_or_404 from django.utils import timezone +from django.utils.decorators import method_decorator +from django.views import View +from django.views.generic import DetailView from analysis.forms import TimespanReportForm from analysis.utils.excel.excel import TempExcelFile @@ -42,57 +46,112 @@ def index_reports_view(request: HttpRequest): context = BaseContext(request, context).context return render(request, template, context) +class ReportIndexView(LoginRequiredMixin, View): + @method_decorator(conservation_office_group_required) + def get(self, request: HttpRequest) -> HttpResponse: -@login_required -@conservation_office_group_required -def detail_report_view(request: HttpRequest, id: str): - """ Renders the detailed report for a conservation office + """ - Args: - request (HttpRequest): The incoming request - id (str): The conservation_office KonovaCode id + Args: + request (HttpRequest): The incoming request - Returns: + Returns: - """ - # Try to resolve the requested office id - cons_office = get_object_or_404( - KonovaCode, - id=id - ) - # Try to resolve the date parameters into Date objects -> redirect if this fails - try: - df = request.GET.get("df", None) - dt = request.GET.get("dt", None) - date_from = timezone.make_aware(timezone.datetime.fromisoformat(df)) - date_to = timezone.make_aware(timezone.datetime.fromisoformat(dt)) - except ValueError: - messages.error( - request, - PARAMS_INVALID, - extra_tags="danger", + """ + template = "analysis/reports/index.html" + form = TimespanReportForm(None) + context = { + "form": form + } + context = BaseContext(request, context).context + return render(request, template, context) + + @method_decorator(conservation_office_group_required) + def post(self, request: HttpRequest) -> HttpResponse: + + """ + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + template = "analysis/reports/index.html" + form = TimespanReportForm(request.POST or None) + if form.is_valid(): + redirect_url = form.save() + return redirect(redirect_url) + else: + messages.error( + request, + FORM_INVALID, + extra_tags="danger", + ) + context = { + "form": form + } + context = BaseContext(request, context).context + return render(request, template, context) + + +class ReportDetailView(LoginRequiredMixin, DetailView): + + @method_decorator(conservation_office_group_required) + def get(self, request: HttpRequest, id: str): + """ Renders the detailed report for a conservation office + + Args: + request (HttpRequest): The incoming request + id (str): The conservation_office KonovaCode id + + Returns: + + """ + # Try to resolve the requested office id + cons_office = get_object_or_404( + KonovaCode, + id=id ) - return redirect("analysis:reports") + # Try to resolve the date parameters into Date objects -> redirect if this fails + try: + df = request.GET.get("df", None) + dt = request.GET.get("dt", None) + date_from = timezone.make_aware(timezone.datetime.fromisoformat(df)) + date_to = timezone.make_aware(timezone.datetime.fromisoformat(dt)) + except ValueError: + messages.error( + request, + PARAMS_INVALID, + extra_tags="danger", + ) + return redirect("analysis:reports") - # Check whether the html default rendering is requested or an alternative - format_param = request.GET.get("format", "html") - report = TimespanReport(id, date_from, date_to) + # Check whether the html default rendering is requested or an alternative + format_param = request.GET.get("format", "html") + report = TimespanReport(id, date_from, date_to) - if format_param == "html": + if format_param == "html": + return self.__handle_html_format(request, report, cons_office) + elif format_param == "excel": + return self.__handle_excel_format(report, cons_office, df, dt) + else: + raise NotImplementedError + + def __handle_html_format(self, request, report: TimespanReport, office: KonovaCode): template = "analysis/reports/detail.html" context = { - "office": cons_office, + "office": office, "report": report, } context = BaseContext(request, context).context return render(request, template, context) - elif format_param == "excel": + + def __handle_excel_format(self, report: TimespanReport, office: KonovaCode, df: str, dt: str): file = TempExcelFile(report.excel_template_path, report.excel_map) response = HttpResponse( content=file.stream, content_type="application/ms-excel", ) - response['Content-Disposition'] = f'attachment; filename={cons_office.long_name}_{df}_{dt}.xlsx' + response['Content-Disposition'] = f'attachment; filename={office.long_name}_{df}_{dt}.xlsx' return response - else: - raise NotImplementedError diff --git a/api/urls/urls.py b/api/urls/urls.py index abe9eacf..fe0ddbbc 100644 --- a/api/urls/urls.py +++ b/api/urls/urls.py @@ -7,11 +7,8 @@ Created on: 21.01.22 """ from django.urls import path, include -from api.views.method_views import generate_new_token_view - app_name = "api" urlpatterns = [ path("v1/", include("api.urls.v1.urls", namespace="v1")), - path("token/generate", generate_new_token_view, name="generate-new-token"), ] \ No newline at end of file diff --git a/api/views/method_views.py b/api/views/method_views.py deleted file mode 100644 index 7c6904a5..00000000 --- a/api/views/method_views.py +++ /dev/null @@ -1,35 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 27.01.22 - -""" -from django.contrib.auth.decorators import login_required -from django.http import HttpRequest, JsonResponse - -from api.models import APIUserToken - - -@login_required -def generate_new_token_view(request: HttpRequest): - """ Handles request for fetching - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - - if request.method == "GET": - token = APIUserToken() - while APIUserToken.objects.filter(token=token.token).exists(): - token = APIUserToken() - return JsonResponse( - data={ - "gen_data": token.token - } - ) - else: - raise NotImplementedError \ No newline at end of file diff --git a/compensation/admin.py b/compensation/admin.py index fc59451f..67ef2f7a 100644 --- a/compensation/admin.py +++ b/compensation/admin.py @@ -45,6 +45,14 @@ class AbstractCompensationAdmin(BaseObjectAdmin): states = "\n".join(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): autocomplete_fields = [ diff --git a/compensation/forms/eco_account.py b/compensation/forms/eco_account.py index 42443025..46e6691c 100644 --- a/compensation/forms/eco_account.py +++ b/compensation/forms/eco_account.py @@ -15,6 +15,7 @@ from compensation.models import EcoAccount from intervention.models import Handler, Responsibility, Legal from konova.forms import SimpleGeomForm from konova.forms.modals import RemoveModalForm +from konova.settings import ETS_GROUP from konova.utils import validators from user.models import User, UserActionLogEntry @@ -246,4 +247,13 @@ class RemoveEcoAccountModalForm(RemoveModalForm): "confirm", _("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 diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py index 45a11594..d7f0df8a 100644 --- a/compensation/urls/compensation.py +++ b/compensation/urls/compensation.py @@ -7,30 +7,32 @@ Created on: 24.08.21 """ from django.urls import path +from compensation.views.compensation.detail import DetailCompensationView from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \ GetCompensationDocumentView, RemoveCompensationDocumentView +from compensation.views.compensation.remove import RemoveCompensationView 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, \ RemoveCompensationDeadlineView from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \ RemoveCompensationActionView from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \ RemoveCompensationStateView -from compensation.views.compensation.compensation import index_view, new_view, new_id_view, detail_view, edit_view, \ - remove_view +from compensation.views.compensation.compensation import IndexCompensationView, CompensationIdentifierGeneratorView, \ + EditCompensationView, NewCompensationView from compensation.views.compensation.log import CompensationLogView urlpatterns = [ # Main compensation - path("", index_view, name="index"), - path('new/id', new_id_view, name='new-id'), - path('new/', new_view, name='new'), - path('new', new_view, name='new'), - path('', detail_view, name='detail'), + path("", IndexCompensationView.as_view(), name="index"), + path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'), + path('new/', NewCompensationView.as_view(), name='new'), + path('new', NewCompensationView.as_view(), name='new'), + path('', DetailCompensationView.as_view(), name='detail'), path('/log', CompensationLogView.as_view(), name='log'), - path('/edit', edit_view, name='edit'), - path('/remove', remove_view, name='remove'), + path('/edit', EditCompensationView.as_view(), name='edit'), + path('/remove', RemoveCompensationView.as_view(), name='remove'), path('/state/new', NewCompensationStateView.as_view(), name='new-state'), path('/state//edit', EditCompensationStateView.as_view(), name='state-edit'), @@ -43,7 +45,7 @@ urlpatterns = [ path('/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"), path('/deadline//edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'), path('/deadline//remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'), - path('/report', report_view, name='report'), + path('/report', CompensationPublicReportView.as_view(), name='report'), path('/resub', CompensationResubmissionView.as_view(), name='resubmission-create'), # Documents diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py index beaae8d9..d57f9b24 100644 --- a/compensation/urls/eco_account.py +++ b/compensation/urls/eco_account.py @@ -8,11 +8,13 @@ Created on: 24.08.21 from django.urls import path from compensation.autocomplete.eco_account import EcoAccountAutocomplete -from compensation.views.eco_account.eco_account import index_view, new_view, new_id_view, edit_view, remove_view, \ - detail_view +from compensation.views.eco_account.detail import DetailEcoAccountView +from compensation.views.eco_account.eco_account import IndexEcoAccountView, EcoAccountIdentifierGeneratorView, \ + NewEcoAccountView, EditEcoAccountView from compensation.views.eco_account.log import EcoAccountLogView from compensation.views.eco_account.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.state import NewEcoAccountStateView, EditEcoAccountStateView, \ RemoveEcoAccountStateView @@ -28,15 +30,15 @@ from compensation.views.eco_account.deduction import NewEcoAccountDeductionView, app_name = "acc" urlpatterns = [ - path("", index_view, name="index"), - path('new/', new_view, name='new'), - path('new/id', new_id_view, name='new-id'), - path('', detail_view, name='detail'), + path("", IndexEcoAccountView.as_view(), name="index"), + path('new/', NewEcoAccountView.as_view(), name='new'), + path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'), + path('', DetailEcoAccountView.as_view(), name='detail'), path('/log', EcoAccountLogView.as_view(), name='log'), path('/record', EcoAccountRecordView.as_view(), name='record'), - path('/report', report_view, name='report'), - path('/edit', edit_view, name='edit'), - path('/remove', remove_view, name='remove'), + path('/report', EcoAccountPublicReportView.as_view(), name='report'), + path('/edit', EditEcoAccountView.as_view(), name='edit'), + path('/remove', RemoveEcoAccountView.as_view(), name='remove'), path('/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'), path('/state/new', NewEcoAccountStateView.as_view(), name='new-state'), diff --git a/compensation/urls/payment.py b/compensation/urls/payment.py index b51384dd..d051423b 100644 --- a/compensation/urls/payment.py +++ b/compensation/urls/payment.py @@ -10,7 +10,7 @@ from compensation.views.payment import * app_name = "pay" urlpatterns = [ - path('/new', new_payment_view, name='new'), - path('/remove/', payment_remove_view, name='remove'), - path('/edit/', payment_edit_view, name='edit'), + path('/new', NewPaymentView.as_view(), name='new'), + path('/remove/', RemovePaymentView.as_view(), name='remove'), + path('/edit/', EditPaymentView.as_view(), name='edit'), ] diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 15bac1f8..147ea038 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -6,91 +6,127 @@ Created on: 19.08.22 """ from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.core.exceptions import ObjectDoesNotExist -from django.db.models import Sum -from django.http import HttpRequest, JsonResponse +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.urls import reverse +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ +from django.views import View from compensation.forms.compensation import EditCompensationForm, NewCompensationForm from compensation.models import Compensation from compensation.tables.compensation import CompensationTable from intervention.models import Intervention from konova.contexts import BaseContext -from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \ - uuid_required +from konova.decorators import shared_access_required, default_group_required 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.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ - RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \ - COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE +from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, \ + IDENTIFIER_REPLACED, COMPENSATION_ADDED_TEMPLATE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE +from konova.views.identifier import AbstractIdentifierGeneratorView +from konova.views.index import AbstractIndexView -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ - Renders the index view for compensation +class IndexCompensationView(AbstractIndexView): + def get(self, request, *args, **kwargs) -> HttpResponse: + """ + Renders the index view for compensation - Args: - request (HttpRequest): The incoming request + Args: + request (HttpRequest): The incoming request - Returns: - A rendered view - """ - template = "generic_index.html" - compensations = Compensation.objects.filter( - deleted=None, # only show those which are not deleted individually - intervention__deleted=None, # and don't show the ones whose intervention has been deleted - ).order_by( - "-modified__timestamp" - ) - table = CompensationTable( - request=request, - queryset=compensations - ) - context = { - "table": table, - TAB_TITLE_IDENTIFIER: _("Compensations - Overview"), - } - context = BaseContext(request, context).context - return render(request, template, context) + Returns: + A rendered view + """ + compensations = Compensation.objects.filter( + deleted=None, # only show those which are not deleted individually + intervention__deleted=None, # and don't show the ones whose intervention has been deleted + ).order_by( + "-modified__timestamp" + ) + table = CompensationTable( + request=request, + queryset=compensations + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: _("Compensations - Overview"), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) -@login_required -@default_group_required -@shared_access_required(Intervention, "intervention_id") -def new_view(request: HttpRequest, intervention_id: str = None): - """ - Renders a view for a new compensation creation +class NewCompensationView(LoginRequiredMixin, View): + _TEMPLATE = "compensation/form/view.html" - Args: - request (HttpRequest): The incoming request + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "intervention_id")) + def get(self, request: HttpRequest, intervention_id: str = None, *args, **kwargs) -> HttpResponse: + """ + Renders a view for new compensation - Returns: + A compensation creation may be called directly from the parent-intervention object. If so - we may take + the intervention's id and directly link the compensation to it. - """ - template = "compensation/form/view.html" - if intervention_id is not None: - try: - intervention = Intervention.objects.get(id=intervention_id) - except ObjectDoesNotExist: - messages.error(request, PARAMS_INVALID) - return redirect("home") - if intervention.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("intervention:detail", id=intervention_id) + Args: + request (HttpRequest): The incoming request + intervention_id (str): The intervention identifier + + Returns: + + """ + if intervention_id: + # If the parent-intervention is recorded, we are not allowed to change anything on it's data. + # Not even adding new child elements like compensations! + intervention = get_object_or_404(Intervention, id=intervention_id) + recording_state_blocks_actions = intervention.is_recorded + if recording_state_blocks_actions: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("intervention:detail", id=intervention_id) + + data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) + + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New compensation"), + } + context = BaseContext(request, context).context + + return render(request, self._TEMPLATE, context) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "intervention_id")) + def post(self, request: HttpRequest, intervention_id: str = None, *args, **kwargs) -> HttpResponse: + + """ + Renders a view for a new compensation creation + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + if intervention_id: + # If the parent-intervention is recorded, we are not allowed to change anything on it's data. + # Not even adding new child elements like compensations! + intervention = get_object_or_404(Intervention, id=intervention_id) + recording_state_blocks_actions = intervention.is_recorded + if recording_state_blocks_actions: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("intervention:detail", id=intervention_id) + + data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) - data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id) - geom_form = SimpleGeomForm(request.POST or None, read_only=False) - if request.method == "POST": if data_form.is_valid() and geom_form.is_valid(): generated_identifier = data_form.cleaned_data.get("identifier", None) comp = data_form.save(request.user, geom_form) @@ -108,80 +144,97 @@ def new_view(request: HttpRequest, intervention_id: str = None): request, GEOMETRY_SIMPLIFIED ) - num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - return redirect("compensation:detail", id=comp.id) else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("New compensation"), - } - context = BaseContext(request, context).context - return render(request, template, context) + messages.error(request, FORM_INVALID, extra_tags="danger", ) - -@login_required -@default_group_required -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 + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New compensation"), } - ) + context = BaseContext(request, context).context + + return render(request, self._TEMPLATE, context) -@login_required -@default_group_required -@shared_access_required(Compensation, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing compensations +class CompensationIdentifierGeneratorView(AbstractIdentifierGeneratorView): + _MODEL = Compensation - Args: - request (HttpRequest): The incoming request - Returns: +class EditCompensationView(LoginRequiredMixin, View): + _TEMPLATE = "compensation/form/view.html" - """ - template = "compensation/form/view.html" - # Get object from db - comp = get_object_or_404(Compensation, id=id) - if comp.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("compensation:detail", id=id) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: - # Create forms, initialize with values from db/from POST request - data_form = EditCompensationForm(request.POST or None, instance=comp) - geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp) - if request.method == "POST": + """ + Renders a view for editing compensations + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + # Get object from db + comp = get_object_or_404(Compensation, id=id) + if comp.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("compensation:detail", id=id) + + # Create forms, initialize with values from db/from POST request + data_form = EditCompensationForm(request.POST or None, instance=comp) + geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp) + + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier), + } + context = BaseContext(request, context).context + + return render(request, self._TEMPLATE, context) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Compensation, "id")) + def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + + """ + Renders a view for editing compensations + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + # Get object from db + comp = get_object_or_404(Compensation, id=id) + if comp.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("compensation:detail", id=id) + + # Create forms, initialize with values from db/from POST request + data_form = EditCompensationForm(request.POST or None, instance=comp) + geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp) if data_form.is_valid() and geom_form.is_valid(): # Preserve state of intervention checked to determine whether the user must be informed or not # about a change of the check state intervention_is_checked = comp.intervention.checked is not None - # The data form takes the geom form for processing, as well as the performing user comp = data_form.save(request.user, geom_form) if intervention_is_checked: @@ -192,126 +245,21 @@ def edit_view(request: HttpRequest, id: str): request, GEOMETRY_SIMPLIFIED ) - num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - return redirect("compensation:detail", id=comp.id) else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier), - } - context = BaseContext(request, context).context - return render(request, template, context) + messages.error(request, FORM_INVALID, extra_tags="danger", ) + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier), + } + context = BaseContext(request, context).context -@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"), - ) - + return render(request, self._TEMPLATE, context) \ No newline at end of file diff --git a/compensation/views/compensation/detail.py b/compensation/views/compensation/detail.py new file mode 100644 index 00000000..6b342119 --- /dev/null +++ b/compensation/views/compensation/detail.py @@ -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) \ No newline at end of file diff --git a/compensation/views/compensation/remove.py b/compensation/views/compensation/remove.py new file mode 100644 index 00000000..a073d089 --- /dev/null +++ b/compensation/views/compensation/remove.py @@ -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) diff --git a/compensation/views/compensation/report.py b/compensation/views/compensation/report.py index 96081627..f7fec85f 100644 --- a/compensation/views/compensation/report.py +++ b/compensation/views/compensation/report.py @@ -5,77 +5,81 @@ Contact: ksp-servicestelle@sgdnord.rlp.de 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.urls import reverse from django.utils.translation import gettext_lazy as _ from compensation.models import Compensation from konova.contexts import BaseContext -from konova.decorators import uuid_required from konova.forms import SimpleGeomForm 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): - """ Renders the public report view - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention +class CompensationPublicReportView(AbstractPublicReportView): + _TEMPLATE = "compensation/report/compensation/report.html" - Returns: + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders the public report view - """ - # Reuse the compensation report template since compensations are structurally identical - template = "compensation/report/compensation/report.html" - comp = get_object_or_404(Compensation, id=id) + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention + + Returns: + + """ + comp = get_object_or_404(Compensation, id=id) + tab_title = _("Report {}").format(comp.identifier) + # If intervention is not recorded (yet or currently) we need to render another template without any data + if not comp.is_ready_for_publish(): + template = "report/unavailable.html" + context = { + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) + + # Prepare data for map viewer + geom_form = SimpleGeomForm( + instance=comp + ) + parcels = comp.get_underlying_parcels() + + qrcode = QrCode( + content=request.build_absolute_uri(reverse("compensation:report", args=(id,))), + size=10 + ) + qrcode_lanis = QrCode( + content=comp.get_LANIS_link(), + size=7 + ) + + # Order states by surface + before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type") + after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type") + actions = comp.actions.all().prefetch_related("action_type") - tab_title = _("Report {}").format(comp.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not comp.is_ready_for_publish(): - template = "report/unavailable.html" context = { + "obj": comp, + "qrcode": { + "img": qrcode.get_img(), + "url": qrcode.get_content(), + }, + "qrcode_lanis": { + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), + }, + "is_entry_shared": False, # disables action buttons during rendering + "before_states": before_states, + "after_states": after_states, + "geom_form": geom_form, + "parcels": parcels, + "actions": actions, + "tables_scrollable": False, TAB_TITLE_IDENTIFIER: tab_title, } context = BaseContext(request, context).context - return render(request, template, context) - - # Prepare data for map viewer - geom_form = SimpleGeomForm( - instance=comp - ) - parcels = comp.get_underlying_parcels() - - qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,))) - qrcode_img = generate_qr_code(qrcode_url, 10) - qrcode_lanis_url = comp.get_LANIS_link() - qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) - - # Order states by surface - before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type") - after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type") - actions = comp.actions.all().prefetch_related("action_type") - - context = { - "obj": comp, - "qrcode": { - "img": qrcode_img, - "url": qrcode_url, - }, - "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url, - }, - "is_entry_shared": False, # disables action buttons during rendering - "before_states": before_states, - "after_states": after_states, - "geom_form": geom_form, - "parcels": parcels, - "actions": actions, - "tables_scrollable": False, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) + return render(request, self._TEMPLATE, context) diff --git a/compensation/views/eco_account/detail.py b/compensation/views/eco_account/detail.py new file mode 100644 index 00000000..500940a9 --- /dev/null +++ b/compensation/views/eco_account/detail.py @@ -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) \ No newline at end of file diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 28dbfc10..251744e4 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -6,72 +6,94 @@ Created on: 19.08.22 """ from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.db.models import Sum -from django.http import HttpRequest, JsonResponse +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest, HttpResponse 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.views import View -from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm, RemoveEcoAccountModalForm +from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm from compensation.models import EcoAccount from compensation.tables.eco_account import EcoAccountTable from konova.contexts import BaseContext -from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \ - uuid_required +from konova.decorators import shared_access_required, default_group_required 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.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \ - IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE +from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, FORM_INVALID, \ + IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE +from konova.views.identifier import AbstractIdentifierGeneratorView +from konova.views.index import AbstractIndexView -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ - Renders the index view for eco accounts +class IndexEcoAccountView(AbstractIndexView): + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """ + Renders the index view for eco accounts - Args: - request (HttpRequest): The incoming request + Args: + request (HttpRequest): The incoming request - Returns: - A rendered view - """ - template = "generic_index.html" - eco_accounts = EcoAccount.objects.filter( - deleted=None, - ).order_by( - "-modified__timestamp" - ) - table = EcoAccountTable( - request=request, - queryset=eco_accounts - ) - context = { - "table": table, - TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"), - } - context = BaseContext(request, context).context - return render(request, template, context) + Returns: + A rendered view + """ + eco_accounts = EcoAccount.objects.filter( + deleted=None, + ).order_by( + "-modified__timestamp" + ) + table = EcoAccountTable( + request=request, + queryset=eco_accounts + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) -@login_required -@default_group_required -def new_view(request: HttpRequest): - """ - Renders a view for a new eco account creation +class NewEcoAccountView(LoginRequiredMixin, View): + _TEMPLATE = "compensation/form/view.html" - Args: - request (HttpRequest): The incoming request + @method_decorator(default_group_required) + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """ + Renders a view for a new eco account creation - Returns: + Args: + request (HttpRequest): The incoming request - """ - template = "compensation/form/view.html" - data_form = NewEcoAccountForm(request.POST or None) - geom_form = SimpleGeomForm(request.POST or None, read_only=False) - if request.method == "POST": + Returns: + + """ + data_form = NewEcoAccountForm(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 Eco-Account"), + } + 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 eco account creation + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + data_form = NewEcoAccountForm(request.POST or None) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) if data_form.is_valid() and geom_form.is_valid(): generated_identifier = data_form.cleaned_data.get("identifier", None) acc = data_form.save(request.user, geom_form) @@ -89,75 +111,92 @@ def new_view(request: HttpRequest): request, GEOMETRY_SIMPLIFIED ) - num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - return redirect("compensation:acc:detail", id=acc.id) else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("New Eco-Account"), - } - context = BaseContext(request, context).context - return render(request, template, context) + messages.error(request, FORM_INVALID, extra_tags="danger", ) - -@login_required -@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 + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New Eco-Account"), } - ) + context = BaseContext(request, context).context + + return render(request, self._TEMPLATE, context) + +class EcoAccountIdentifierGeneratorView(AbstractIdentifierGeneratorView): + _MODEL = EcoAccount -@login_required -@default_group_required -@shared_access_required(EcoAccount, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing compensations +class EditEcoAccountView(LoginRequiredMixin, View): + _TEMPLATE = "compensation/form/view.html" - Args: - request (HttpRequest): The incoming request + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: - Returns: + """ + Renders a view for editing compensations - """ - template = "compensation/form/view.html" - # Get object from db - acc = get_object_or_404(EcoAccount, id=id) - if acc.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("compensation:acc:detail", id=id) + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + # Get object from db + acc = get_object_or_404(EcoAccount, id=id) + if acc.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("compensation:acc:detail", id=id) + + # Create forms, initialize with values from db/from POST request + data_form = EditEcoAccountForm(request.POST or None, instance=acc) + geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc) + + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(EcoAccount, "id")) + def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + + """ + Renders a view for editing compensations + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + # Get object from db + acc = get_object_or_404(EcoAccount, id=id) + if acc.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("compensation:acc:detail", id=id) + + # Create forms, initialize with values from db/from POST request + data_form = EditEcoAccountForm(request.POST or None, instance=acc) + geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc) - # Create forms, initialize with values from db/from POST request - data_form = EditEcoAccountForm(request.POST or None, instance=acc) - geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc) - if request.method == "POST": data_form_valid = data_form.is_valid() geom_form_valid = geom_form.is_valid() if data_form_valid and geom_form_valid: @@ -169,139 +208,21 @@ def edit_view(request: HttpRequest, id: str): request, GEOMETRY_SIMPLIFIED ) - num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - return redirect("compensation:acc:detail", id=acc.id) else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier), - } - context = BaseContext(request, context).context - return render(request, template, context) + messages.error(request, FORM_INVALID, extra_tags="danger", ) + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier), + } + context = BaseContext(request, context).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"), - ) - + return render(request, self._TEMPLATE, context) diff --git a/compensation/views/eco_account/remove.py b/compensation/views/eco_account/remove.py new file mode 100644 index 00000000..b1ab0334 --- /dev/null +++ b/compensation/views/eco_account/remove.py @@ -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) diff --git a/compensation/views/eco_account/report.py b/compensation/views/eco_account/report.py index f61a7bfc..beb53cd9 100644 --- a/compensation/views/eco_account/report.py +++ b/compensation/views/eco_account/report.py @@ -5,85 +5,88 @@ Contact: ksp-servicestelle@sgdnord.rlp.de 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.urls import reverse from django.utils.translation import gettext_lazy as _ from compensation.models import EcoAccount from konova.contexts import BaseContext -from konova.decorators import uuid_required from konova.forms import SimpleGeomForm 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): - """ Renders the public report view +class EcoAccountPublicReportView(AbstractPublicReportView): + _TEMPLATE = "compensation/report/eco_account/report.html" - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders the public report view - Returns: + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention - """ - # 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) + Returns: + + """ + acc = get_object_or_404(EcoAccount, id=id) + tab_title = _("Report {}").format(acc.identifier) + # If intervention is not recorded (yet or currently) we need to render another template without any data + if not acc.is_ready_for_publish(): + template = "report/unavailable.html" + context = { + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) + + # Prepare data for map viewer + geom_form = SimpleGeomForm( + instance=acc + ) + parcels = acc.get_underlying_parcels() + + qrcode = QrCode( + content=request.build_absolute_uri(reverse("compensation:acc:report", args=(id,))), + size=10 + ) + qrcode_lanis = QrCode( + content=acc.get_LANIS_link(), + size=7 + ) + + # Order states by surface + before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent") + after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent") + actions = acc.actions.all().prefetch_related("action_type__parent") + + # Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier) + deductions = acc.deductions.all() \ + .distinct("intervention") \ + .select_related("intervention") \ + .values_list("intervention__id", "intervention__identifier", "intervention__title", named=True) - tab_title = _("Report {}").format(acc.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not acc.is_ready_for_publish(): - template = "report/unavailable.html" context = { + "obj": acc, + "qrcode": { + "img": qrcode.get_img(), + "url": qrcode.get_content(), + }, + "qrcode_lanis": { + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), + }, + "is_entry_shared": False, # disables action buttons during rendering + "before_states": before_states, + "after_states": after_states, + "geom_form": geom_form, + "parcels": parcels, + "actions": actions, + "deductions": deductions, + "tables_scrollable": False, TAB_TITLE_IDENTIFIER: tab_title, } context = BaseContext(request, context).context - return render(request, template, context) - - # Prepare data for map viewer - geom_form = SimpleGeomForm( - instance=acc - ) - parcels = acc.get_underlying_parcels() - - qrcode_url = request.build_absolute_uri(reverse("compensation:acc:report", args=(id,))) - qrcode_img = generate_qr_code(qrcode_url, 10) - qrcode_lanis_url = acc.get_LANIS_link() - qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) - - # Order states by surface - before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent") - after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent") - actions = acc.actions.all().prefetch_related("action_type__parent") - - # Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier) - deductions = acc.deductions.all()\ - .distinct("intervention")\ - .select_related("intervention")\ - .values_list("intervention__id", "intervention__identifier", "intervention__title", named=True) - - context = { - "obj": acc, - "qrcode": { - "img": qrcode_img, - "url": qrcode_url, - }, - "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url, - }, - "is_entry_shared": False, # disables action buttons during rendering - "before_states": before_states, - "after_states": after_states, - "geom_form": geom_form, - "parcels": parcels, - "actions": actions, - "deductions": deductions, - "tables_scrollable": False, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) + return render(request, self._TEMPLATE, context) diff --git a/compensation/views/payment.py b/compensation/views/payment.py index 29e89ac3..3059bc06 100644 --- a/compensation/views/payment.py +++ b/compensation/views/payment.py @@ -5,10 +5,12 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 09.08.21 """ +from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse -from django.contrib.auth.decorators import login_required from django.http import HttpRequest from django.shortcuts import get_object_or_404 +from django.utils.decorators import method_decorator +from django.views import View from compensation.forms.modals.payment import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm from compensation.models import Payment @@ -17,72 +19,97 @@ from konova.decorators import default_group_required, shared_access_required from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def new_payment_view(request: HttpRequest, id: str): - """ Renders a modal view for adding new payments +class NewPaymentView(LoginRequiredMixin, View): - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id for which a new payment shall be added + def __process_request(self, request: HttpRequest, id: str): + """ Renders a modal view for adding new payments - Returns: + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id for which a new payment shall be added - """ - intervention = get_object_or_404(Intervention, id=id) - form = NewPaymentForm(request.POST or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=PAYMENT_ADDED, - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" - ) + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + form = NewPaymentForm(request.POST or None, instance=intervention, request=request) + return form.process_request( + request, + msg_success=PAYMENT_ADDED, + redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" + ) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str): + return self.__process_request(request, id=id) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str): + return self.__process_request(request, id=id) -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def payment_remove_view(request: HttpRequest, id: str, payment_id: str): - """ Renders a modal view for removing payments +class RemovePaymentView(LoginRequiredMixin, View): - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - payment_id (str): The payment's id + def __process_request(self, request: HttpRequest, id: str, payment_id: str): + """ Renders a modal view for removing payments - Returns: + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id + payment_id (str): The payment's id - """ - intervention = get_object_or_404(Intervention, id=id) - payment = get_object_or_404(Payment, id=payment_id) - form = RemovePaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request) - return form.process_request( - request=request, - msg_success=PAYMENT_REMOVED, - redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data" - ) + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + payment = get_object_or_404(Payment, id=payment_id) + form = RemovePaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request) + return form.process_request( + request=request, + msg_success=PAYMENT_REMOVED, + redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data" + ) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, payment_id: str): + return self.__process_request(request, id=id, payment_id=payment_id) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str, payment_id: str): + return self.__process_request(request, id=id, payment_id=payment_id) -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def payment_edit_view(request: HttpRequest, id: str, payment_id: str): - """ Renders a modal view for editing payments +class EditPaymentView(LoginRequiredMixin, View): + def __process_request(self, request: HttpRequest, id: str, payment_id: str): + """ Renders a modal view for editing payments - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - payment_id (str): The payment's id + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id + payment_id (str): The payment's id - Returns: + Returns: - """ - intervention = get_object_or_404(Intervention, id=id) - payment = get_object_or_404(Payment, id=payment_id) - form = EditPaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request) - return form.process_request( - request=request, - msg_success=PAYMENT_EDITED, - redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data" - ) + """ + intervention = get_object_or_404(Intervention, id=id) + payment = get_object_or_404(Payment, id=payment_id) + form = EditPaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request) + return form.process_request( + request=request, + msg_success=PAYMENT_EDITED, + redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data" + ) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, payment_id: str): + return self.__process_request(request, id=id, payment_id=payment_id) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str, payment_id: str): + return self.__process_request(request, id=id, payment_id=payment_id) diff --git a/ema/templates/ema/detail/includes/controls.html b/ema/templates/ema/detail/includes/controls.html index 96c7bbb3..672cdb22 100644 --- a/ema/templates/ema/detail/includes/controls.html +++ b/ema/templates/ema/detail/includes/controls.html @@ -15,10 +15,10 @@ - {% if is_ets_member %} + {% if obj.recorded %} {% endif %} + + + {% endif %} {% if is_default_member %} - - + {% endif %} + {% if is_ets_member %} + - - - {% endif %} {% endif %} \ No newline at end of file diff --git a/ema/tests/test_views.py b/ema/tests/test_views.py index 627753ac..e58ff42d 100644 --- a/ema/tests/test_views.py +++ b/ema/tests/test_views.py @@ -118,6 +118,7 @@ class EmaViewTestCase(CompensationViewTestCase): self.index_url, self.detail_url, self.report_url, + self.log_url, ] fail_urls = [ self.new_url, @@ -133,7 +134,6 @@ class EmaViewTestCase(CompensationViewTestCase): self.action_remove_url, self.action_new_url, self.new_doc_url, - self.log_url, self.remove_url, ] self.assert_url_fail(client, fail_urls) diff --git a/ema/urls.py b/ema/urls.py index bff7c41d..a36cbe5c 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -9,26 +9,28 @@ from django.urls import path from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView 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.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.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.share import EmaShareFormView, EmaShareByTokenView from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView app_name = "ema" urlpatterns = [ - path("", index_view, name="index"), - path("new/", new_view, name="new"), - path("new/id", new_id_view, name="new-id"), - path("", detail_view, name="detail"), + path("", IndexEmaView.as_view(), name="index"), + path("new/", NewEmaView.as_view(), name="new"), + path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"), + path("", DetailEmaView.as_view(), name="detail"), path('/log', EmaLogView.as_view(), name='log'), - path('/edit', edit_view, name='edit'), - path('/remove', remove_view, name='remove'), + path('/edit', EditEmaView.as_view(), name='edit'), + path('/remove', RemoveEmaView.as_view(), name='remove'), path('/record', EmaRecordView.as_view(), name='record'), - path('/report', report_view, name='report'), + path('/report', EmaPublicReportView.as_view(), name='report'), path('/resub', EmaResubmissionView.as_view(), name='resubmission-create'), path('/state/new', NewEmaStateView.as_view(), name='new-state'), diff --git a/ema/views/detail.py b/ema/views/detail.py new file mode 100644 index 00000000..16d10125 --- /dev/null +++ b/ema/views/detail.py @@ -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) diff --git a/ema/views/ema.py b/ema/views/ema.py index 5322b5ef..c9ee4889 100644 --- a/ema/views/ema.py +++ b/ema/views/ema.py @@ -7,71 +7,96 @@ Created on: 19.08.22 """ from django.contrib import messages from django.contrib.auth.decorators import login_required -from django.db.models import Sum -from django.http import HttpRequest, JsonResponse +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest, HttpResponse 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.views.generic.base import View from ema.forms import NewEmaForm, EditEmaForm from ema.models import Ema from ema.tables import EmaTable from konova.contexts import BaseContext -from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal, \ - uuid_required +from konova.decorators import shared_access_required, conservation_office_group_required 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.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 -def index_view(request: HttpRequest): - """ Renders the index view for EMAs +class IndexEmaView(AbstractIndexView): + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """ Renders the index view for EMAs - Args: - request (HttpRequest): The incoming request + Args: + request (HttpRequest): The incoming request - Returns: + Returns: - """ - template = "generic_index.html" - emas = Ema.objects.filter( - deleted=None, - ).order_by( - "-modified__timestamp" - ) + """ + emas = Ema.objects.filter( + deleted=None, + ).order_by( + "-modified__timestamp" + ) - table = EmaTable( - request, - queryset=emas - ) - context = { - "table": table, - TAB_TITLE_IDENTIFIER: _("EMAs - Overview"), - } - context = BaseContext(request, context).context - return render(request, template, context) + table = EmaTable( + request, + queryset=emas + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: _("EMAs - Overview"), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) +class NewEmaView(LoginRequiredMixin, View): + _TEMPLATE = "ema/form/view.html" -@login_required -@conservation_office_group_required -def new_view(request: HttpRequest): - """ - Renders a view for a new eco account creation + @method_decorator(conservation_office_group_required) + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """ GET endpoint - Args: - request (HttpRequest): The incoming request + Renders form for new EMA - Returns: + Args: + request (HttpRequest): The incoming request + *args (): + **kwargs (): - """ - template = "ema/form/view.html" - data_form = NewEmaForm(request.POST or None) - geom_form = SimpleGeomForm(request.POST or None, read_only=False) - if request.method == "POST": + Returns: + + """ + data_form = NewEmaForm(request.POST or None) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New EMA"), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + @method_decorator(conservation_office_group_required) + def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """ POST endpoint + + Processes submitted form + + Args: + request (HttpRequest): The incoming request + *args (): + **kwargs (): + + Returns: + + """ + data_form = NewEmaForm(request.POST or None) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) if data_form.is_valid() and geom_form.is_valid(): generated_identifier = data_form.cleaned_data.get("identifier", None) ema = data_form.save(request.user, geom_form) @@ -95,128 +120,91 @@ def new_view(request: HttpRequest): request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - return redirect("ema:detail", id=ema.id) else: messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("New EMA"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@conservation_office_group_required -def new_id_view(request: HttpRequest): - """ JSON endpoint - - Provides fetching of free identifiers for e.g. AJAX calls - - """ - tmp = Ema() - identifier = tmp.generate_new_identifier() - while Ema.objects.filter(identifier=identifier).exists(): - identifier = tmp.generate_new_identifier() - return JsonResponse( - data={ - "gen_data": identifier + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New EMA"), } - ) + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) +class EmaIdentifierGeneratorView(AbstractIdentifierGeneratorView): + _MODEL = Ema -@login_required -@uuid_required -def detail_view(request: HttpRequest, id: str): - """ Renders the detail view of an EMA + @method_decorator(conservation_office_group_required) + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + return super().get(request, *args, **kwargs) - Args: - request (HttpRequest): The incoming request - id (str): The EMA id +class EditEmaView(LoginRequiredMixin, View): + _TEMPLATE = "compensation/form/view.html" - Returns: + @method_decorator(conservation_office_group_required) + @method_decorator(shared_access_required(Ema, "id")) + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ GET endpoint - """ - template = "ema/detail/view.html" - ema = get_object_or_404(Ema, id=id, deleted=None) + Renders form - geom_form = SimpleGeomForm(instance=ema) - parcels = ema.get_underlying_parcels() - _user = request.user - is_entry_shared = ema.is_shared_with(_user) + Args: + request (HttpRequest): The incoming request + id (str): The ema identifier + *args (): + **kwargs (): - # Order states according to surface - before_states = ema.before_states.all().order_by("-surface") - after_states = ema.after_states.all().order_by("-surface") + Returns: - # 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) + """ + # Get object from db + ema = get_object_or_404(Ema, id=id) + if ema.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("ema:detail", id=id) - ema.set_status_messages(request) + # Create forms, initialize with values from db/from POST request + data_form = EditEmaForm(instance=ema) + geom_form = SimpleGeomForm(read_only=False, instance=ema) + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) - 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 - ) + @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 - 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, template, context) + Process submitted forms + Args: + request (HttpRequest): The incoming request + id (str): The id of the ema + *args (): + **kwargs (): -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing compensations + Returns: - Args: - request (HttpRequest): The incoming request + """ + # Get object from db + ema = get_object_or_404(Ema, id=id) + if ema.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("ema:detail", id=id) - Returns: - - """ - template = "compensation/form/view.html" - # Get object from db - ema = get_object_or_404(Ema, id=id) - if ema.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("ema:detail", id=id) - - # Create forms, initialize with values from db/from POST request - data_form = EditEmaForm(request.POST or None, instance=ema) - geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema) - if request.method == "POST": + # Create forms, initialize with values from db/from POST request + data_form = EditEmaForm(request.POST or None, instance=ema) + geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema) if data_form.is_valid() and geom_form.is_valid(): # The data form takes the geom form for processing, as well as the performing user ema = data_form.save(request.user, geom_form) @@ -226,48 +214,19 @@ def edit_view(request: HttpRequest, id: str): request, GEOMETRY_SIMPLIFIED ) - num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - return redirect("ema:detail", id=ema.id) else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier), - } - context = BaseContext(request, context).context - return render(request, 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"), - ) - + messages.error(request, FORM_INVALID, extra_tags="danger", ) + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) diff --git a/ema/views/log.py b/ema/views/log.py index 82162ba4..b468cc15 100644 --- a/ema/views/log.py +++ b/ema/views/log.py @@ -18,7 +18,6 @@ class EmaLogView(AbstractLogView): @method_decorator(login_required_modal) @method_decorator(login_required) - @method_decorator(conservation_office_group_required) @method_decorator(shared_access_required(Ema, "id")) def dispatch(self, request, *args, **kwargs): return super().dispatch(request, *args, **kwargs) diff --git a/ema/views/remove.py b/ema/views/remove.py new file mode 100644 index 00000000..07abfae8 --- /dev/null +++ b/ema/views/remove.py @@ -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) diff --git a/ema/views/report.py b/ema/views/report.py index 93af6211..058b93b7 100644 --- a/ema/views/report.py +++ b/ema/views/report.py @@ -5,77 +5,81 @@ Contact: ksp-servicestelle@sgdnord.rlp.de 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.urls import reverse from django.utils.translation import gettext_lazy as _ from ema.models import Ema from konova.contexts import BaseContext -from konova.decorators import uuid_required from konova.forms import SimpleGeomForm 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): - """ Renders the public report view - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention +class EmaPublicReportView(AbstractPublicReportView): + _TEMPLATE = "ema/report/report.html" - Returns: + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders the public report view - """ - # Reuse the compensation report template since EMAs are structurally identical - template = "ema/report/report.html" - ema = get_object_or_404(Ema, id=id) + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + tab_title = _("Report {}").format(ema.identifier) + # If intervention is not recorded (yet or currently) we need to render another template without any data + if not ema.is_ready_for_publish(): + template = "report/unavailable.html" + context = { + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) + + # Prepare data for map viewer + geom_form = SimpleGeomForm( + instance=ema, + ) + parcels = ema.get_underlying_parcels() + + qrcode = QrCode( + content=request.build_absolute_uri(reverse("ema:report", args=(id,))), + size=10 + ) + qrcode_lanis = QrCode( + content=ema.get_LANIS_link(), + size=7 + ) + + # Order states by surface + before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type") + after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type") + actions = ema.actions.all().prefetch_related("action_type") - tab_title = _("Report {}").format(ema.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not ema.is_ready_for_publish(): - template = "report/unavailable.html" context = { + "obj": ema, + "qrcode": { + "img": qrcode.get_img(), + "url": qrcode.get_content(), + }, + "qrcode_lanis": { + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), + }, + "is_entry_shared": False, # disables action buttons during rendering + "before_states": before_states, + "after_states": after_states, + "geom_form": geom_form, + "parcels": parcels, + "actions": actions, + "tables_scrollable": False, TAB_TITLE_IDENTIFIER: tab_title, } context = BaseContext(request, context).context - return render(request, template, context) - - # Prepare data for map viewer - geom_form = SimpleGeomForm( - instance=ema, - ) - parcels = ema.get_underlying_parcels() - - qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,))) - qrcode_img = generate_qr_code(qrcode_url, 10) - qrcode_lanis_url = ema.get_LANIS_link() - qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) - - # Order states by surface - before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type") - after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type") - actions = ema.actions.all().prefetch_related("action_type") - - context = { - "obj": ema, - "qrcode": { - "img": qrcode_img, - "url": qrcode_url - }, - "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url - }, - "is_entry_shared": False, # disables action buttons during rendering - "before_states": before_states, - "after_states": after_states, - "geom_form": geom_form, - "parcels": parcels, - "actions": actions, - "tables_scrollable": False, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) + return render(request, self._TEMPLATE, context) \ No newline at end of file diff --git a/intervention/admin.py b/intervention/admin.py index 46f7244d..208030e6 100644 --- a/intervention/admin.py +++ b/intervention/admin.py @@ -37,6 +37,14 @@ class InterventionAdmin(BaseObjectAdmin): "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): pass diff --git a/intervention/urls.py b/intervention/urls.py index 8a148197..a2fff6fc 100644 --- a/intervention/urls.py +++ b/intervention/urls.py @@ -8,39 +8,42 @@ Created on: 30.11.20 from django.urls import path from intervention.autocomplete.intervention import InterventionAutocomplete -from intervention.views.check import check_view -from intervention.views.compensation import remove_compensation_view +from intervention.views.check import InterventionCheckView +from intervention.views.compensation import RemoveCompensationFromInterventionView from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \ RemoveInterventionDeductionView from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \ 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.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.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \ - get_revocation_view +from intervention.views.revocation import NewInterventionRevocationView, GetInterventionRevocationView, \ + EditInterventionRevocationView, RemoveInterventionRevocationView from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView app_name = "intervention" urlpatterns = [ - path("", index_view, name="index"), - path('new/', new_view, name='new'), - path('new/id', new_id_view, name='new-id'), - path('', detail_view, name='detail'), + path("", IndexInterventionView.as_view(), name="index"), + path('new/', NewInterventionView.as_view(), name='new'), + path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'), + path('', DetailInterventionView.as_view(), name='detail'), path('/log', InterventionLogView.as_view(), name='log'), - path('/edit', edit_view, name='edit'), - path('/remove', remove_view, name='remove'), + path('/edit', EditInterventionView.as_view(), name='edit'), + path('/remove', RemoveInterventionView.as_view(), name='remove'), path('/share/', InterventionShareByTokenView.as_view(), name='share-token'), path('/share', InterventionShareFormView.as_view(), name='share-form'), - path('/check', check_view, name='check'), + path('/check', InterventionCheckView.as_view(), name='check'), path('/record', InterventionRecordView.as_view(), name='record'), - path('/report', report_view, name='report'), + path('/report', InterventionPublicReportView.as_view(), name='report'), path('/resub', InterventionResubmissionView.as_view(), name='resubmission-create'), # Compensations - path('/compensation//remove', remove_compensation_view, name='remove-compensation'), + path('/compensation//remove', RemoveCompensationFromInterventionView.as_view(), name='remove-compensation'), # Documents path('/document/new/', NewInterventionDocumentView.as_view(), name='new-doc'), @@ -54,10 +57,10 @@ urlpatterns = [ path('/deduction//remove', RemoveInterventionDeductionView.as_view(), name='remove-deduction'), # Revocation routes - path('/revocation/new', new_revocation_view, name='new-revocation'), - path('/revocation//edit', edit_revocation_view, name='edit-revocation'), - path('/revocation//remove', remove_revocation_view, name='remove-revocation'), - path('revocation/', get_revocation_view, name='get-doc-revocation'), + path('/revocation/new', NewInterventionRevocationView.as_view(), name='new-revocation'), + path('/revocation//edit', EditInterventionRevocationView.as_view(), name='edit-revocation'), + path('/revocation//remove', RemoveInterventionRevocationView.as_view(), name='remove-revocation'), + path('revocation/', GetInterventionRevocationView.as_view(), name='get-doc-revocation'), # Autocomplete path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="autocomplete"), diff --git a/intervention/views/check.py b/intervention/views/check.py index 1fae75bb..a217f989 100644 --- a/intervention/views/check.py +++ b/intervention/views/check.py @@ -5,35 +5,44 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required -from django.http import HttpRequest +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404 +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ +from django.views import View from intervention.forms.modals.check import CheckModalForm from intervention.models import Intervention from konova.decorators import registration_office_group_required, shared_access_required from konova.utils.message_templates import INTERVENTION_INVALID +class InterventionCheckView(LoginRequiredMixin, View): -@login_required -@registration_office_group_required -@shared_access_required(Intervention, "id") -def check_view(request: HttpRequest, id: str): - """ Renders check form for an intervention + def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders check form for an intervention - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id + Args: + request (HttpRequest): The incoming request + id (str): Intervention's id - Returns: + Returns: - """ - intervention = get_object_or_404(Intervention, id=id) - form = CheckModalForm(request.POST or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=_("Check performed"), - msg_error=INTERVENTION_INVALID - ) + """ + intervention = get_object_or_404(Intervention, id=id) + form = CheckModalForm(request.POST or None, instance=intervention, request=request) + return form.process_request( + request, + msg_success=_("Check performed"), + msg_error=INTERVENTION_INVALID + ) + @method_decorator(registration_office_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, *args, **kwargs) + + @method_decorator(registration_office_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, *args, **kwargs) diff --git a/intervention/views/compensation.py b/intervention/views/compensation.py index 704a04e4..b8516d38 100644 --- a/intervention/views/compensation.py +++ b/intervention/views/compensation.py @@ -5,42 +5,50 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 19.08.22 """ -from django.contrib.auth.decorators import login_required +from django.contrib.auth.mixins import LoginRequiredMixin from django.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.urls import reverse +from django.utils.decorators import method_decorator +from django.views import View 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.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE -@login_required_modal -@login_required -@shared_access_required(Intervention, "id") -def remove_compensation_view(request: HttpRequest, id: str, comp_id: str): - """ Renders a modal view for removing the compensation +class RemoveCompensationFromInterventionView(LoginRequiredMixin, View): - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id + def __process_request(self, request: HttpRequest, id: str, comp_id: str, *args, **kwargs) -> HttpResponse: + """ Renders a modal view for removing the compensation - Returns: + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id - """ - intervention = get_object_or_404(Intervention, id=id) - try: - comp = intervention.compensations.get( - id=comp_id + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + try: + comp = intervention.compensations.get( + id=comp_id + ) + except ObjectDoesNotExist: + raise Http404("Unknown compensation") + form = RemoveModalForm(request.POST or None, instance=comp, request=request) + return form.process_request( + request=request, + msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier), + redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data", ) - except ObjectDoesNotExist: - raise Http404("Unknown compensation") - form = RemoveModalForm(request.POST or None, instance=comp, request=request) - return form.process_request( - request=request, - msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier), - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data", - ) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request, id: str, comp_id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, comp_id, *args, **kwargs) + + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request, id: str, comp_id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, comp_id, *args, **kwargs) \ No newline at end of file diff --git a/intervention/views/detail.py b/intervention/views/detail.py new file mode 100644 index 00000000..68f98708 --- /dev/null +++ b/intervention/views/detail.py @@ -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) diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index 0d6cc369..f8808914 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -7,76 +7,98 @@ Created on: 19.08.22 """ from django.contrib import messages 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.urls import reverse +from django.utils.decorators import method_decorator from django.utils.translation import gettext_lazy as _ +from django.views import View from intervention.forms.intervention import EditInterventionForm, NewInterventionForm from intervention.models import Intervention from intervention.tables import InterventionTable from konova.contexts import BaseContext -from konova.decorators import default_group_required, shared_access_required, any_group_check, login_required_modal, \ - uuid_required +from konova.decorators import default_group_required, shared_access_required 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.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ - CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \ +from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, \ + CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, \ GEOMETRIES_IGNORED_TEMPLATE +from konova.views.identifier import AbstractIdentifierGeneratorView +from konova.views.index import AbstractIndexView -@login_required -@any_group_check -def index_view(request: HttpRequest): - """ - Renders the index view for Interventions +class IndexInterventionView(AbstractIndexView): + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: + """ + Renders the index view for Interventions - Args: - request (HttpRequest): The incoming request + Args: + request (HttpRequest): The incoming request - Returns: - A rendered view - """ - template = "generic_index.html" - - # Filtering by user access is performed in table filter inside InterventionTableFilter class - interventions = Intervention.objects.filter( - deleted=None, # not deleted - ).select_related( - "legal" - ).order_by( - "-modified__timestamp" - ) - table = InterventionTable( - request=request, - queryset=interventions - ) - context = { - "table": table, - TAB_TITLE_IDENTIFIER: _("Interventions - Overview"), - } - context = BaseContext(request, context).context - return render(request, template, context) + Returns: + A rendered view + """ + # Filtering by user access is performed in table filter inside InterventionTableFilter class + interventions = Intervention.objects.filter( + deleted=None, # not deleted + ).select_related( + "legal" + ).order_by( + "-modified__timestamp" + ) + table = InterventionTable( + request=request, + queryset=interventions + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: _("Interventions - Overview"), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) -@login_required -@default_group_required -def new_view(request: HttpRequest): - """ - Renders a view for a new intervention creation +class NewInterventionView(LoginRequiredMixin, View): + _TEMPLATE = "intervention/form/view.html" - Args: - request (HttpRequest): The incoming request + @method_decorator(default_group_required) + def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: - Returns: + """ + 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 + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + data_form = NewInterventionForm(request.POST or None) + geom_form = SimpleGeomForm(request.POST or None, read_only=False) - """ - template = "intervention/form/view.html" - data_form = NewInterventionForm(request.POST or None) - geom_form = SimpleGeomForm(request.POST or None, read_only=False) - if request.method == "POST": if data_form.is_valid() and geom_form.is_valid(): generated_identifier = data_form.cleaned_data.get("identifier", None) intervention = data_form.save(request.user, geom_form) @@ -88,6 +110,7 @@ def new_view(request: HttpRequest): intervention.identifier ) ) + messages.success(request, _("Intervention {} added").format(intervention.identifier)) if geom_form.has_geometry_simplified(): messages.info( @@ -101,142 +124,86 @@ def new_view(request: HttpRequest): request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - return redirect("intervention:detail", id=intervention.id) + else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("New intervention"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@default_group_required -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 + messages.error(request, FORM_INVALID, extra_tags="danger", ) + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("New intervention"), } - ) + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) -@login_required -@any_group_check -@uuid_required -def detail_view(request: HttpRequest, id: str): - """ Renders a detail view for viewing an intervention's data - - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id - - 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) +class InterventionIdentifierGeneratorView(AbstractIdentifierGeneratorView): + _MODEL = Intervention -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing interventions +class EditInterventionView(LoginRequiredMixin, View): + _TEMPLATE = "intervention/form/view.html" - Args: - request (HttpRequest): The incoming request + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ + Renders a view for editing interventions - Returns: + Args: + request (HttpRequest): The incoming request + id (str): The intervention identifier - """ - template = "intervention/form/view.html" - # Get object from db - intervention = get_object_or_404(Intervention, id=id) - if intervention.is_recorded: - messages.info( - request, - RECORDED_BLOCKS_EDIT - ) - return redirect("intervention:detail", id=id) + Returns: + HttpResponse: The rendered view + """ - # Create forms, initialize with values from db/from POST request - data_form = EditInterventionForm(request.POST or None, instance=intervention) - geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention) - if request.method == "POST": + # Get object from db + intervention = get_object_or_404(Intervention, id=id) + if intervention.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("intervention:detail", id=id) + + # Create forms, initialize with values from db/from POST request + data_form = EditInterventionForm(request.POST or None, instance=intervention) + geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention) + context = { + "form": data_form, + "geom_form": geom_form, + TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier), + } + context = BaseContext(request, context).context + return render(request, self._TEMPLATE, context) + + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ + Process saved form content + + Args: + request (HttpRequest): The incoming request + id (str): The intervention id + + Returns: + HttpResponse: + """ + # Get object from db + intervention = get_object_or_404(Intervention, id=id) + if intervention.is_recorded: + messages.info( + request, + RECORDED_BLOCKS_EDIT + ) + return redirect("intervention:detail", id=id) + + # Create forms, initialize with values from db/from POST request + data_form = EditInterventionForm(request.POST or None, instance=intervention) + geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention) if data_form.is_valid() and geom_form.is_valid(): # 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 @@ -250,48 +217,17 @@ def edit_view(request: HttpRequest, id: str): request, GEOMETRY_SIMPLIFIED ) - num_ignored_geometries = geom_form.get_num_geometries_ignored() if num_ignored_geometries > 0: messages.info( request, GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries) ) - return redirect("intervention:detail", id=intervention.id) - else: - messages.error(request, FORM_INVALID, extra_tags="danger",) - else: - # For clarification: nothing in this case - pass - context = { - "form": data_form, - "geom_form": geom_form, - TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@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") - ) + 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) diff --git a/intervention/views/remove.py b/intervention/views/remove.py new file mode 100644 index 00000000..defe8730 --- /dev/null +++ b/intervention/views/remove.py @@ -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) diff --git a/intervention/views/report.py b/intervention/views/report.py index 6bdd8252..708717a7 100644 --- a/intervention/views/report.py +++ b/intervention/views/report.py @@ -5,72 +5,78 @@ Contact: ksp-servicestelle@sgdnord.rlp.de 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.urls import reverse from django.utils.translation import gettext_lazy as _ from intervention.models import Intervention from konova.contexts import BaseContext -from konova.decorators import uuid_required from konova.forms import SimpleGeomForm 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): - """ Renders the public report view +class InterventionPublicReportView(AbstractPublicReportView): + _TEMPLATE = "intervention/report/report.html" - Args: - request (HttpRequest): The incoming request - id (str): The id of the intervention + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders the public report view - Returns: + Args: + request (HttpRequest): The incoming request + id (str): The id of the intervention - """ - template = "intervention/report/report.html" - intervention = get_object_or_404(Intervention, id=id) + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + + tab_title = _("Report {}").format(intervention.identifier) + # If intervention is not recorded (yet or currently) we need to render another template without any data + if not intervention.is_ready_for_publish(): + template = "report/unavailable.html" + context = { + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) + + # Prepare data for map viewer + geom_form = SimpleGeomForm( + instance=intervention + ) + parcels = intervention.get_underlying_parcels() + + distinct_deductions = intervention.deductions.all().distinct( + "account" + ) + + qrcode = QrCode( + content=request.build_absolute_uri(reverse("intervention:report", args=(id,))), + size=10 + ) + qrcode_lanis = QrCode( + content=intervention.get_LANIS_link(), + size=7 + ) - tab_title = _("Report {}").format(intervention.identifier) - # If intervention is not recorded (yet or currently) we need to render another template without any data - if not intervention.is_ready_for_publish(): - template = "report/unavailable.html" context = { + "obj": intervention, + "deductions": distinct_deductions, + "qrcode": { + "img": qrcode.get_img(), + "url": qrcode.get_content(), + }, + "qrcode_lanis": { + "img": qrcode_lanis.get_img(), + "url": qrcode_lanis.get_content(), + }, + "geom_form": geom_form, + "parcels": parcels, + "tables_scrollable": False, TAB_TITLE_IDENTIFIER: tab_title, } context = BaseContext(request, context).context - return render(request, template, context) - - # Prepare data for map viewer - geom_form = SimpleGeomForm( - instance=intervention - ) - parcels = intervention.get_underlying_parcels() - - distinct_deductions = intervention.deductions.all().distinct( - "account" - ) - qrcode_url = request.build_absolute_uri(reverse("intervention:report", args=(id,))) - qrcode_img = generate_qr_code(qrcode_url, 10) - qrcode_lanis_url = intervention.get_LANIS_link() - qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7) - - context = { - "obj": intervention, - "deductions": distinct_deductions, - "qrcode": { - "img": qrcode_img, - "url": qrcode_url, - }, - "qrcode_lanis": { - "img": qrcode_img_lanis, - "url": qrcode_lanis_url, - }, - "geom_form": geom_form, - "parcels": parcels, - "tables_scrollable": False, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) + return render(request, self._TEMPLATE, context) \ No newline at end of file diff --git a/intervention/views/revocation.py b/intervention/views/revocation.py index db1dd1d2..0f1db94f 100644 --- a/intervention/views/revocation.py +++ b/intervention/views/revocation.py @@ -6,10 +6,12 @@ Created on: 19.08.22 """ from django.contrib import messages -from django.contrib.auth.decorators import login_required -from django.http import HttpRequest +from django.contrib.auth.mixins import LoginRequiredMixin +from django.http import HttpRequest, HttpResponse from django.shortcuts import get_object_or_404, redirect from django.urls import reverse +from django.utils.decorators import method_decorator +from django.views import View from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \ RemoveRevocationModalForm @@ -19,100 +21,125 @@ from konova.utils.documents import get_document from konova.utils.message_templates import REVOCATION_ADDED, DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def new_revocation_view(request: HttpRequest, id: str): - """ Renders sharing form for an intervention +class NewInterventionRevocationView(LoginRequiredMixin, View): + def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + """ Renders sharing form for an intervention - Args: - request (HttpRequest): The incoming request - id (str): Intervention's id + Args: + request (HttpRequest): The incoming request + id (str): Intervention's id - Returns: + Returns: - """ - intervention = get_object_or_404(Intervention, id=id) - form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, request=request) - return form.process_request( - request, - msg_success=REVOCATION_ADDED, - redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@default_group_required -def get_revocation_view(request: HttpRequest, doc_id: str): - """ Returns the revocation document as downloadable file - - Wraps the generic document fetcher function from konova.utils. - - Args: - request (HttpRequest): The incoming request - doc_id (str): The document id - - Returns: - - """ - doc = get_object_or_404(RevocationDocument, id=doc_id) - # File download only possible if related instance is shared with user - if not doc.instance.legal.intervention.users.filter(id=request.user.id): - messages.info( + """ + intervention = get_object_or_404(Intervention, id=id) + form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, + request=request) + return form.process_request( request, - DATA_UNSHARED + msg_success=REVOCATION_ADDED, + redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data" ) - return redirect("intervention:detail", id=doc.instance.id) - return get_document(doc) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, *args, **kwargs) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, *args, **kwargs) -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def edit_revocation_view(request: HttpRequest, id: str, revocation_id: str): - """ Renders a edit view for a revocation +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 - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id as string - revocation_id (str): The revocation's id as string + Wraps the generic document fetcher function from konova.utils. - Returns: + Args: + request (HttpRequest): The incoming request + doc_id (str): The document id - """ - intervention = get_object_or_404(Intervention, id=id) - revocation = get_object_or_404(Revocation, id=revocation_id) + Returns: - form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, revocation=revocation, request=request) - return form.process_request( - request, - REVOCATION_EDITED, - redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" - ) + """ + doc = get_object_or_404(RevocationDocument, id=doc_id) + # File download only possible if related instance is shared with user + if not doc.instance.legal.intervention.users.filter(id=request.user.id): + messages.info( + request, + DATA_UNSHARED + ) + return redirect("intervention:detail", id=doc.instance.id) + return get_document(doc) -@login_required_modal -@login_required -@default_group_required -@shared_access_required(Intervention, "id") -def remove_revocation_view(request: HttpRequest, id: str, revocation_id: str): - """ Renders a remove view for a revocation +class EditInterventionRevocationView(LoginRequiredMixin, View): + def __process_request(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse: + """ Renders a edit view for a revocation - Args: - request (HttpRequest): The incoming request - id (str): The intervention's id as string - revocation_id (str): The revocation's id as string + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id as string + revocation_id (str): The revocation's id as string - Returns: + Returns: - """ - intervention = get_object_or_404(Intervention, id=id) - revocation = get_object_or_404(Revocation, id=revocation_id) + """ + intervention = get_object_or_404(Intervention, id=id) + revocation = get_object_or_404(Revocation, id=revocation_id) - form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation, request=request) - return form.process_request( - request, - REVOCATION_REMOVED, - redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" - ) + form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, + revocation=revocation, request=request) + return form.process_request( + request, + REVOCATION_EDITED, + redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" + ) + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, revocation_id, *args, **kwargs) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, revocation_id, *args, **kwargs) + + +class RemoveInterventionRevocationView(LoginRequiredMixin, View): + def __process_request(self, request, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse: + """ Renders a remove view for a revocation + + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id as string + revocation_id (str): The revocation's id as string + + Returns: + + """ + intervention = get_object_or_404(Intervention, id=id) + revocation = get_object_or_404(Revocation, id=revocation_id) + + form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation, + request=request) + return form.process_request( + request, + REVOCATION_REMOVED, + redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" + ) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def get(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, revocation_id, *args, **kwargs) + + @method_decorator(default_group_required) + @method_decorator(shared_access_required(Intervention, "id")) + def post(self, request, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse: + return self.__process_request(request, id, revocation_id, *args, **kwargs) \ No newline at end of file diff --git a/konova/utils/generators.py b/konova/utils/generators.py index 78e075ad..d1980bc2 100644 --- a/konova/utils/generators.py +++ b/konova/utils/generators.py @@ -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)) return ret_val +class IdentifierGenerator: + _MODEL = None -def generate_qr_code(content: str, size: int = 20) -> str: - """ Generates a qr code from given content + def __init__(self, model): + from konova.models import BaseObject + if not issubclass(model, BaseObject): + raise AssertionError("Model must be a subclass of BaseObject!") - Args: - content (str): The content for the qr code - size (int): The image size + self._MODEL = model - Returns: - qrcode_svg (str): The qr code as svg - """ - qrcode_factory = qrcode.image.svg.SvgImage - qrcode_img = qrcode.make( - content, - image_factory=qrcode_factory, - box_size=size - ) - stream = BytesIO() - qrcode_img.save(stream) - return stream.getvalue().decode() + def generate_id(self) -> str: + """ Generates a unique identifier + + Returns: + + """ + unpersisted_object = self._MODEL() + identifier = unpersisted_object.generate_new_identifier() + while self._MODEL.objects.filter(identifier=identifier).exists(): + identifier = unpersisted_object.generate_new_identifier() + return identifier diff --git a/konova/utils/qrcode.py b/konova/utils/qrcode.py new file mode 100644 index 00000000..e1498553 --- /dev/null +++ b/konova/utils/qrcode.py @@ -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 diff --git a/konova/views/detail.py b/konova/views/detail.py new file mode 100644 index 00000000..49e0156c --- /dev/null +++ b/konova/views/detail.py @@ -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() diff --git a/konova/views/identifier.py b/konova/views/identifier.py new file mode 100644 index 00000000..ddacbd3f --- /dev/null +++ b/konova/views/identifier.py @@ -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 + } + ) \ No newline at end of file diff --git a/konova/views/index.py b/konova/views/index.py new file mode 100644 index 00000000..228c019a --- /dev/null +++ b/konova/views/index.py @@ -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() diff --git a/konova/views/remove.py b/konova/views/remove.py new file mode 100644 index 00000000..24093677 --- /dev/null +++ b/konova/views/remove.py @@ -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) diff --git a/konova/views/report.py b/konova/views/report.py new file mode 100644 index 00000000..4e1efb28 --- /dev/null +++ b/konova/views/report.py @@ -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() diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 4f5c1c6c..2cbacbbc 100644 Binary files a/locale/de/LC_MESSAGES/django.mo and b/locale/de/LC_MESSAGES/django.mo differ diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index 6f308995..a3759066 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -45,7 +45,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\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" "Last-Translator: FULL NAME \n" "Language-Team: LANGUAGE \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" #: compensation/forms/compensation.py:114 -#: compensation/views/compensation/compensation.py:120 +#: compensation/views/compensation/compensation.py:121 msgid "New compensation" msgstr "Neue Kompensation" @@ -456,38 +456,38 @@ msgstr "Neue Kompensation" msgid "Edit compensation" 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" 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" 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/utils/quality.py:84 msgid "Agreement date" msgstr "Vereinbarungsdatum" -#: compensation/forms/eco_account.py:45 +#: compensation/forms/eco_account.py:46 msgid "When did the parties agree on this?" msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?" -#: compensation/forms/eco_account.py:72 -#: compensation/views/eco_account/eco_account.py:101 +#: compensation/forms/eco_account.py:73 +#: compensation/views/eco_account/eco_account.py:105 msgid "New Eco-Account" msgstr "Neues Ökokonto" -#: compensation/forms/eco_account.py:81 +#: compensation/forms/eco_account.py:82 msgid "Eco-Account XY; Location ABC" msgstr "Ökokonto XY; Flur ABC" -#: compensation/forms/eco_account.py:147 +#: compensation/forms/eco_account.py:148 msgid "Edit Eco-Account" msgstr "Ökokonto bearbeiten" -#: compensation/forms/eco_account.py:183 +#: compensation/forms/eco_account.py:184 msgid "" "{}m² have been deducted from this eco account so far. The given value of {} " "would be too low." @@ -495,12 +495,16 @@ msgstr "" "{}n² wurden bereits von diesem Ökokonto abgebucht. Der eingegebene Wert von " "{} 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." msgstr "" "Das Ökokonto kann nicht entfernt werden, da hierzu noch Abbuchungen " "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/templates/compensation/detail/eco_account/view.html:63 #: compensation/templates/compensation/report/eco_account/report.html:20 @@ -1288,44 +1292,40 @@ msgstr "" msgid "Responsible data" msgstr "Daten zu den verantwortlichen Stellen" -#: compensation/views/compensation/compensation.py:58 +#: compensation/views/compensation/compensation.py:52 msgid "Compensations - Overview" msgstr "Kompensationen - Übersicht" -#: compensation/views/compensation/compensation.py:181 +#: compensation/views/compensation/compensation.py:167 #: konova/utils/message_templates.py:40 msgid "Compensation {} edited" msgstr "Kompensation {} bearbeitet" -#: compensation/views/compensation/compensation.py:196 -#: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:238 -#: intervention/views/intervention.py:253 +#: compensation/views/compensation/compensation.py:190 +#: compensation/views/eco_account/eco_account.py:168 ema/views/ema.py:173 +#: intervention/views/intervention.py:175 msgid "Edit {}" msgstr "Bearbeite {}" #: compensation/views/compensation/report.py:35 -#: compensation/views/eco_account/report.py:36 ema/views/report.py:35 -#: intervention/views/report.py:35 +#: compensation/views/eco_account/report.py:35 ema/views/report.py:35 +#: intervention/views/report.py:36 msgid "Report {}" msgstr "Bericht {}" -#: compensation/views/eco_account/eco_account.py:53 +#: compensation/views/eco_account/eco_account.py:49 msgid "Eco-account - Overview" msgstr "Ökokonten - Übersicht" -#: compensation/views/eco_account/eco_account.py:86 +#: compensation/views/eco_account/eco_account.py:82 msgid "Eco-Account {} added" msgstr "Ökokonto {} hinzugefügt" -#: compensation/views/eco_account/eco_account.py:158 +#: compensation/views/eco_account/eco_account.py:145 msgid "Eco-Account {} edited" msgstr "Ökokonto {} bearbeitet" -#: compensation/views/eco_account/eco_account.py:288 -msgid "Eco-account removed" -msgstr "Ökokonto entfernt" - -#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:108 +#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:107 msgid "New EMA" msgstr "Neue EMA hinzufügen" @@ -1353,22 +1353,18 @@ msgstr "" msgid "Payment funded compensation" msgstr "Ersatzzahlungsmaßnahme" -#: ema/views/ema.py:53 +#: ema/views/ema.py:52 msgid "EMAs - Overview" msgstr "EMAs - Übersicht" -#: ema/views/ema.py:86 +#: ema/views/ema.py:85 msgid "EMA {} added" msgstr "EMA {} hinzugefügt" -#: ema/views/ema.py:223 +#: ema/views/ema.py:150 msgid "EMA {} edited" msgstr "EMA {} bearbeitet" -#: ema/views/ema.py:262 -msgid "EMA removed" -msgstr "EMA entfernt" - #: intervention/forms/intervention.py:49 msgid "Construction XY; Location ABC" msgstr "Bauvorhaben XY; Flur ABC" @@ -1429,7 +1425,7 @@ msgstr "Datum Bestandskraft bzw. Rechtskraft" #: intervention/forms/intervention.py:216 #: intervention/tests/unit/test_forms.py:36 -#: intervention/views/intervention.py:105 +#: intervention/views/intervention.py:109 msgid "New intervention" msgstr "Neuer Eingriff" @@ -1665,22 +1661,18 @@ msgstr "" msgid "Check performed" msgstr "Prüfung durchgeführt" -#: intervention/views/intervention.py:57 +#: intervention/views/intervention.py:53 msgid "Interventions - Overview" msgstr "Eingriffe - Übersicht" -#: intervention/views/intervention.py:90 +#: intervention/views/intervention.py:86 msgid "Intervention {} added" msgstr "Eingriff {} hinzugefügt" -#: intervention/views/intervention.py:236 +#: intervention/views/intervention.py:150 msgid "Intervention {} edited" msgstr "Eingriff {} bearbeitet" -#: intervention/views/intervention.py:278 -msgid "{} removed" -msgstr "{} entfernt" - #: konova/decorators.py:32 msgid "You need to be staff to perform this action!" msgstr "Hierfür müssen Sie Mitarbeiter sein!" @@ -1810,7 +1802,7 @@ msgstr "Nicht editierbar" msgid "Geometry" msgstr "Geometrie" -#: konova/forms/geometry_form.py:100 +#: konova/forms/geometry_form.py:105 msgid "Only surfaces allowed. Points or lines must be buffered." msgstr "" "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 " "stored geometry." msgstr "" -"Die Geometrie enthielt {} invalide Bestandteile (z.B. unaussagekräftige Kleinstflächen)." -"Diese Bestandteile wurden automatisch entfernt. Bitte überprüfen Sie die angepasste Geometrie." +"Die Geometrie enthielt {} invalide Bestandteile (z.B. unaussagekräftige " +"Kleinstflächen).Diese Bestandteile wurden automatisch entfernt. Bitte " +"überprüfen Sie die angepasste Geometrie." #: konova/utils/message_templates.py:89 msgid "This intervention has {} revocations" @@ -2330,6 +2323,10 @@ msgstr "{} verzeichnet" msgid "Errors found:" msgstr "Fehler gefunden:" +#: konova/views/remove.py:35 +msgid "{} removed" +msgstr "{} entfernt" + #: konova/views/resubmission.py:39 msgid "Resubmission set" msgstr "Wiedervorlage gesetzt"