diff --git a/ema/urls.py b/ema/urls.py index 63073d6..9287727 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -6,7 +6,17 @@ Created on: 19.08.21 """ from django.urls import path -from ema.views import * + +from ema.views.action import action_new_view, action_edit_view, action_remove_view +from ema.views.deadline import deadline_new_view, deadline_edit_view, deadline_remove_view +from ema.views.document import document_new_view, get_document_view, remove_document_view, edit_document_view +from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_view, remove_view +from ema.views.log import log_view +from ema.views.record import record_view +from ema.views.report import report_view +from ema.views.resubmission import create_resubmission_view +from ema.views.share import share_view, create_share_view +from ema.views.state import state_new_view, state_remove_view, state_edit_view app_name = "ema" urlpatterns = [ diff --git a/ema/views.py b/ema/views.py deleted file mode 100644 index 2dd6e51..0000000 --- a/ema/views.py +++ /dev/null @@ -1,739 +0,0 @@ -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.shortcuts import render, get_object_or_404, redirect -from django.urls import reverse -from django.utils.translation import gettext_lazy as _ - -from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \ - EditCompensationActionModalForm, RemoveCompensationActionModalForm -from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm -from compensation.forms.modals.state import NewCompensationStateModalForm, RemoveCompensationStateModalForm, \ - EditCompensationStateModalForm -from compensation.models import CompensationAction, CompensationState -from ema.forms import NewEmaForm, EditEmaForm, NewEmaDocumentModalForm -from ema.tables import EmaTable -from intervention.forms.modals.share import ShareModalForm -from konova.contexts import BaseContext -from konova.decorators import conservation_office_group_required, shared_access_required -from ema.models import Ema, EmaDocument -from konova.forms.modals import RemoveModalForm, RecordModalForm, RemoveDeadlineModalForm, \ - EditDocumentModalForm, ResubmissionModalForm -from konova.forms import SimpleGeomForm -from konova.models import Deadline -from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP -from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER -from konova.utils.documents import get_document, remove_document -from konova.utils.generators import generate_qr_code -from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \ - DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, \ - COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, \ - COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, RECORDED_BLOCKS_EDIT -from konova.utils.user_checks import in_group - - -@login_required -def index_view(request: HttpRequest): - """ Renders the index view for EMAs - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "generic_index.html" - emas = Ema.objects.filter( - deleted=None, - ).order_by( - "-modified" - ) - table = EmaTable( - request, - queryset=emas - ) - context = { - "table": table, - TAB_TITLE_IDENTIFIER: _("EMAs - Overview"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@conservation_office_group_required -def new_view(request: HttpRequest): - """ - Renders a view for a new eco account creation - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - 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": - if data_form.is_valid() and geom_form.is_valid(): - generated_identifier = data_form.cleaned_data.get("identifier", None) - ema = data_form.save(request.user, geom_form) - if generated_identifier != ema.identifier: - messages.info( - request, - IDENTIFIER_REPLACED.format( - generated_identifier, - ema.identifier - ) - ) - messages.success(request, _("EMA {} added").format(ema.identifier)) - 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 - } - ) - - -@login_required -def detail_view(request: HttpRequest, id: str): - """ Renders the detail view of an EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA id - - Returns: - - """ - template = "ema/detail/view.html" - ema = get_object_or_404(Ema, id=id, deleted=None) - - geom_form = SimpleGeomForm(instance=ema) - parcels = ema.get_underlying_parcels() - _user = request.user - is_data_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 = before_states.aggregate(Sum("surface"))["surface__sum"] or 0 - sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 - diff_states = abs(sum_before_states - sum_after_states) - - ema.set_status_messages(request) - - context = { - "obj": ema, - "geom_form": geom_form, - "parcels": parcels, - "has_access": 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, - "is_default_member": in_group(_user, DEFAULT_GROUP), - "is_zb_member": in_group(_user, ZB_GROUP), - "is_ets_member": in_group(_user, 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) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def log_view(request: HttpRequest, id: str): - """ Renders a log view using modal - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - template = "modal/modal_generic.html" - body_template = "log.html" - - context = { - "modal_body_template": body_template, - "log": ema.log.all(), - "modal_title": _("Log"), - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def edit_view(request: HttpRequest, id: str): - """ - Renders a view for editing compensations - - Args: - request (HttpRequest): The incoming request - - Returns: - - """ - template = "compensation/form/view.html" - # Get object from db - 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": - if data_form.is_valid() and geom_form.is_valid(): - # The data form takes the geom form for processing, as well as the performing user - ema = data_form.save(request.user, geom_form) - messages.success(request, _("EMA {} edited").format(ema.identifier)) - 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 -@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"), - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def record_view(request: HttpRequest, id: str): - """ Renders a modal view for recording the EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - msg_succ = _("{} unrecorded") if ema.recorded else _("{} recorded") - form = RecordModalForm(request.POST or None, instance=ema, request=request) - return form.process_request( - request=request, - msg_success=msg_succ.format("EMA"), - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def state_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for an EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id to which the new state will be related - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = NewCompensationStateModalForm(request.POST or None, instance=ema, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_ADDED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def action_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new actions for an EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id to which the new state will be related - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = NewCompensationActionModalForm(request.POST or None, instance=ema, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_ADDED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def action_edit_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for editing an actions for an EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id - action_id (str): The action id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = EditCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_EDITED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def deadline_new_view(request: HttpRequest, id: str): - """ Renders a form for adding new states for an EMA - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id to which the new state will be related - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = NewDeadlineModalForm(request.POST or None, instance=ema, request=request) - return form.process_request( - request, - msg_success=DEADLINE_ADDED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def document_new_view(request: HttpRequest, id: str): - """ Renders a form for uploading new documents - - Args: - request (HttpRequest): The incoming request - id (str): The EMA's id to which the new document will be related - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = NewEmaDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, request=request) - return form.process_request( - request, - msg_success=DOCUMENT_ADDED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def get_document_view(request: HttpRequest, id: str, doc_id: str): - """ Returns the document as downloadable file - - Wraps the generic document fetcher function from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The EMA id - doc_id (str): The document id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - doc = get_object_or_404(EmaDocument, id=doc_id) - return get_document(doc) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def edit_document_view(request: HttpRequest, id: str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The EMA id - doc_id (str): The document id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - doc = get_object_or_404(EmaDocument, id=doc_id) - form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, document=doc, request=request) - return form.process_request( - request, - DOCUMENT_EDITED, - reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def remove_document_view(request: HttpRequest, id:str, doc_id: str): - """ Removes the document from the database and file system - - Wraps the generic functionality from konova.utils. - - Args: - request (HttpRequest): The incoming request - id (str): The EMA id - doc_id (str): The document id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - doc = get_object_or_404(EmaDocument, id=doc_id) - return remove_document( - request, - doc - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def state_remove_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for removing an EMA state - - Args: - request (HttpRequest): The incoming request - id (str): The ema id - state_id (str): The state's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = RemoveCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_REMOVED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def state_edit_view(request: HttpRequest, id: str, state_id: str): - """ Renders a form for editing an EMA state - - Args: - request (HttpRequest): The incoming request - id (str): The ema id - state_id (str): The state's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - state = get_object_or_404(CompensationState, id=state_id) - form = EditCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_STATE_EDITED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def action_remove_view(request: HttpRequest, id: str, action_id: str): - """ Renders a form for removing an EMA action - - Args: - request (HttpRequest): The incoming request - id (str): The ema id - id (str): The action's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - action = get_object_or_404(CompensationAction, id=action_id) - form = RemoveCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request) - return form.process_request( - request, - msg_success=COMPENSATION_ACTION_REMOVED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -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 - - Returns: - - """ - # Reuse the compensation report template since EMAs are structurally identical - template = "ema/report/report.html" - 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_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 - }, - "has_access": False, # disables action buttons during rendering - "before_states": before_states, - "after_states": after_states, - "geom_form": geom_form, - "parcels": parcels, - "actions": actions, - TAB_TITLE_IDENTIFIER: tab_title, - } - context = BaseContext(request, context).context - return render(request, template, context) - - -@login_required -def share_view(request: HttpRequest, id: str, token: str): - """ Performs sharing of an ema - - If token given in url is not valid, the user will be redirected to the dashboard - - Args: - request (HttpRequest): The incoming request - id (str): EMA's id - token (str): Access token for EMA - - Returns: - - """ - user = request.user - obj = get_object_or_404(Ema, id=id) - # Check tokens - if obj.access_token == token: - # Send different messages in case user has already been added to list of sharing users - if obj.is_shared_with(user): - messages.info( - request, - _("{} has already been shared with you").format(obj.identifier) - ) - else: - messages.success( - request, - _("{} has been shared with you").format(obj.identifier) - ) - obj.share_with_user(user) - return redirect("ema:detail", id=id) - else: - messages.error( - request, - _("Share link invalid"), - extra_tags="danger", - ) - return redirect("home") - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def create_share_view(request: HttpRequest, id: str): - """ Renders sharing form for an Ema - - Args: - request (HttpRequest): The incoming request - id (str): Ema's id - - Returns: - - """ - obj = get_object_or_404(Ema, id=id) - form = ShareModalForm(request.POST or None, instance=obj, request=request) - return form.process_request( - request, - msg_success=_("Share settings updated") - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for editing deadlines from a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = EditDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_EDITED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): - """ Renders a form for removing deadlines from a compensation - - Args: - request (HttpRequest): The incoming request - id (str): The compensation's id - deadline_id (str): The deadline's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - deadline = get_object_or_404(Deadline, id=deadline_id) - form = RemoveDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request) - return form.process_request( - request, - msg_success=DEADLINE_REMOVED, - redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" - ) - - -@login_required -@conservation_office_group_required -@shared_access_required(Ema, "id") -def create_resubmission_view(request: HttpRequest, id: str): - """ Renders resubmission form for an EMA - - Args: - request (HttpRequest): The incoming request - id (str): EMA's id - - Returns: - - """ - ema = get_object_or_404(Ema, id=id) - form = ResubmissionModalForm(request.POST or None, instance=ema, request=request) - form.action_url = reverse("ema:resubmission-create", args=(id,)) - return form.process_request( - request, - msg_success=_("Resubmission set"), - redirect_url=reverse("ema:detail", args=(id,)) - ) diff --git a/ema/views/__init__.py b/ema/views/__init__.py new file mode 100644 index 0000000..3a996e1 --- /dev/null +++ b/ema/views/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" diff --git a/ema/views/action.py b/ema/views/action.py new file mode 100644 index 0000000..bd13646 --- /dev/null +++ b/ema/views/action.py @@ -0,0 +1,89 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +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.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.compensation_action import RemoveCompensationActionModalForm, \ + EditCompensationActionModalForm, NewCompensationActionModalForm +from compensation.models import CompensationAction +from ema.models import Ema +from konova.decorators import shared_access_required, conservation_office_group_required +from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_EDITED, \ + COMPENSATION_ACTION_ADDED + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def action_new_view(request: HttpRequest, id: str): + """ Renders a form for adding new actions for an EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id to which the new state will be related + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + form = NewCompensationActionModalForm(request.POST or None, instance=ema, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_ACTION_ADDED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def action_edit_view(request: HttpRequest, id: str, action_id: str): + """ Renders a form for editing an actions for an EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id + action_id (str): The action id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + action = get_object_or_404(CompensationAction, id=action_id) + form = EditCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_ACTION_EDITED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def action_remove_view(request: HttpRequest, id: str, action_id: str): + """ Renders a form for removing an EMA action + + Args: + request (HttpRequest): The incoming request + id (str): The ema id + id (str): The action's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + action = get_object_or_404(CompensationAction, id=action_id) + form = RemoveCompensationActionModalForm(request.POST or None, instance=ema, action=action, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_ACTION_REMOVED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) diff --git a/ema/views/deadline.py b/ema/views/deadline.py new file mode 100644 index 0000000..223ceeb --- /dev/null +++ b/ema/views/deadline.py @@ -0,0 +1,90 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +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.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.deadline import EditDeadlineModalForm, NewDeadlineModalForm +from ema.models import Ema +from konova.decorators import shared_access_required, conservation_office_group_required +from konova.forms.modals import RemoveDeadlineModalForm +from konova.models import Deadline +from konova.utils.message_templates import DEADLINE_REMOVED, DEADLINE_EDITED, DEADLINE_ADDED + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def deadline_new_view(request: HttpRequest, id: str): + """ Renders a form for adding new states for an EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id to which the new state will be related + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + form = NewDeadlineModalForm(request.POST or None, instance=ema, request=request) + return form.process_request( + request, + msg_success=DEADLINE_ADDED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def deadline_edit_view(request: HttpRequest, id: str, deadline_id: str): + """ Renders a form for editing deadlines from a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + deadline_id (str): The deadline's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + deadline = get_object_or_404(Deadline, id=deadline_id) + form = EditDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request) + return form.process_request( + request, + msg_success=DEADLINE_EDITED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str): + """ Renders a form for removing deadlines from a compensation + + Args: + request (HttpRequest): The incoming request + id (str): The compensation's id + deadline_id (str): The deadline's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + deadline = get_object_or_404(Deadline, id=deadline_id) + form = RemoveDeadlineModalForm(request.POST or None, instance=ema, deadline=deadline, request=request) + return form.process_request( + request, + msg_success=DEADLINE_REMOVED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + diff --git a/ema/views/document.py b/ema/views/document.py new file mode 100644 index 0000000..faacb5b --- /dev/null +++ b/ema/views/document.py @@ -0,0 +1,111 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +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.shortcuts import get_object_or_404 +from django.urls import reverse + +from ema.forms import NewEmaDocumentModalForm +from ema.models import Ema, EmaDocument +from konova.decorators import shared_access_required, conservation_office_group_required +from konova.forms.modals import EditDocumentModalForm +from konova.utils.documents import get_document, remove_document +from konova.utils.message_templates import DOCUMENT_ADDED, DOCUMENT_EDITED + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def document_new_view(request: HttpRequest, id: str): + """ Renders a form for uploading new documents + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id to which the new document will be related + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + form = NewEmaDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, request=request) + return form.process_request( + request, + msg_success=DOCUMENT_ADDED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def get_document_view(request: HttpRequest, id: str, doc_id: str): + """ Returns the document as downloadable file + + Wraps the generic document fetcher function from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The EMA id + doc_id (str): The document id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + doc = get_object_or_404(EmaDocument, id=doc_id) + return get_document(doc) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def edit_document_view(request: HttpRequest, id: str, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The EMA id + doc_id (str): The document id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + doc = get_object_or_404(EmaDocument, id=doc_id) + form = EditDocumentModalForm(request.POST or None, request.FILES or None, instance=ema, document=doc, request=request) + return form.process_request( + request, + DOCUMENT_EDITED, + reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def remove_document_view(request: HttpRequest, id:str, doc_id: str): + """ Removes the document from the database and file system + + Wraps the generic functionality from konova.utils. + + Args: + request (HttpRequest): The incoming request + id (str): The EMA id + doc_id (str): The document id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + doc = get_object_or_404(EmaDocument, id=doc_id) + return remove_document( + request, + doc + ) + diff --git a/ema/views/ema.py b/ema/views/ema.py new file mode 100644 index 0000000..f2b4d3b --- /dev/null +++ b/ema/views/ema.py @@ -0,0 +1,237 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.db.models import Sum +from django.http import HttpRequest, JsonResponse +from django.shortcuts import get_object_or_404, redirect, render +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from ema.forms import NewEmaForm, EditEmaForm +from ema.models import Ema +from ema.tables import EmaTable +from konova.contexts import BaseContext +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 +from konova.utils.user_checks import in_group + + +@login_required +def index_view(request: HttpRequest): + """ Renders the index view for EMAs + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + template = "generic_index.html" + emas = Ema.objects.filter( + deleted=None, + ).order_by( + "-modified" + ) + table = EmaTable( + request, + queryset=emas + ) + context = { + "table": table, + TAB_TITLE_IDENTIFIER: _("EMAs - Overview"), + } + context = BaseContext(request, context).context + return render(request, template, context) + + +@login_required +@conservation_office_group_required +def new_view(request: HttpRequest): + """ + Renders a view for a new eco account creation + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + 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": + if data_form.is_valid() and geom_form.is_valid(): + generated_identifier = data_form.cleaned_data.get("identifier", None) + ema = data_form.save(request.user, geom_form) + if generated_identifier != ema.identifier: + messages.info( + request, + IDENTIFIER_REPLACED.format( + generated_identifier, + ema.identifier + ) + ) + messages.success(request, _("EMA {} added").format(ema.identifier)) + 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 + } + ) + + +@login_required +def detail_view(request: HttpRequest, id: str): + """ Renders the detail view of an EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA id + + Returns: + + """ + template = "ema/detail/view.html" + ema = get_object_or_404(Ema, id=id, deleted=None) + + geom_form = SimpleGeomForm(instance=ema) + parcels = ema.get_underlying_parcels() + _user = request.user + is_data_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 = before_states.aggregate(Sum("surface"))["surface__sum"] or 0 + sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 + diff_states = abs(sum_before_states - sum_after_states) + + ema.set_status_messages(request) + + context = { + "obj": ema, + "geom_form": geom_form, + "parcels": parcels, + "has_access": 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, + "is_default_member": in_group(_user, DEFAULT_GROUP), + "is_zb_member": in_group(_user, ZB_GROUP), + "is_ets_member": in_group(_user, 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) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def edit_view(request: HttpRequest, id: str): + """ + Renders a view for editing compensations + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + template = "compensation/form/view.html" + # Get object from db + 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": + if data_form.is_valid() and geom_form.is_valid(): + # The data form takes the geom form for processing, as well as the performing user + ema = data_form.save(request.user, geom_form) + messages.success(request, _("EMA {} edited").format(ema.identifier)) + 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 +@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"), + ) + diff --git a/ema/views/log.py b/ema/views/log.py new file mode 100644 index 0000000..3e3f869 --- /dev/null +++ b/ema/views/log.py @@ -0,0 +1,41 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +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.shortcuts import get_object_or_404, render +from django.utils.translation import gettext_lazy as _ + +from ema.models import Ema +from konova.contexts import BaseContext +from konova.decorators import shared_access_required, conservation_office_group_required + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def log_view(request: HttpRequest, id: str): + """ Renders a log view using modal + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + template = "modal/modal_generic.html" + body_template = "log.html" + + context = { + "modal_body_template": body_template, + "log": ema.log.all(), + "modal_title": _("Log"), + } + context = BaseContext(request, context).context + return render(request, template, context) diff --git a/ema/views/record.py b/ema/views/record.py new file mode 100644 index 0000000..4ae40d2 --- /dev/null +++ b/ema/views/record.py @@ -0,0 +1,37 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +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.shortcuts import get_object_or_404 +from django.utils.translation import gettext_lazy as _ + +from ema.models import Ema +from konova.decorators import shared_access_required, conservation_office_group_required +from konova.forms.modals import RecordModalForm + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def record_view(request: HttpRequest, id: str): + """ Renders a modal view for recording the EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + msg_succ = _("{} unrecorded") if ema.recorded else _("{} recorded") + form = RecordModalForm(request.POST or None, instance=ema, request=request) + return form.process_request( + request=request, + msg_success=msg_succ.format("EMA"), + ) diff --git a/ema/views/report.py b/ema/views/report.py new file mode 100644 index 0000000..daea0b4 --- /dev/null +++ b/ema/views/report.py @@ -0,0 +1,79 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.http import HttpRequest +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.forms import SimpleGeomForm +from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER +from konova.utils.generators import generate_qr_code + + +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 + + Returns: + + """ + # Reuse the compensation report template since EMAs are structurally identical + template = "ema/report/report.html" + 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_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 + }, + "has_access": False, # disables action buttons during rendering + "before_states": before_states, + "after_states": after_states, + "geom_form": geom_form, + "parcels": parcels, + "actions": actions, + TAB_TITLE_IDENTIFIER: tab_title, + } + context = BaseContext(request, context).context + return render(request, template, context) diff --git a/ema/views/resubmission.py b/ema/views/resubmission.py new file mode 100644 index 0000000..838cab3 --- /dev/null +++ b/ema/views/resubmission.py @@ -0,0 +1,39 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +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.shortcuts import get_object_or_404 +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from ema.models import Ema +from konova.decorators import shared_access_required, conservation_office_group_required +from konova.forms.modals import ResubmissionModalForm + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def create_resubmission_view(request: HttpRequest, id: str): + """ Renders resubmission form for an EMA + + Args: + request (HttpRequest): The incoming request + id (str): EMA's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + form = ResubmissionModalForm(request.POST or None, instance=ema, request=request) + form.action_url = reverse("ema:resubmission-create", args=(id,)) + return form.process_request( + request, + msg_success=_("Resubmission set"), + redirect_url=reverse("ema:detail", args=(id,)) + ) diff --git a/ema/views/share.py b/ema/views/share.py new file mode 100644 index 0000000..83aae91 --- /dev/null +++ b/ema/views/share.py @@ -0,0 +1,77 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 19.08.22 + +""" +from django.contrib import messages +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest +from django.shortcuts import get_object_or_404, redirect +from django.utils.translation import gettext_lazy as _ + +from ema.models import Ema +from intervention.forms.modals.share import ShareModalForm +from konova.decorators import conservation_office_group_required, shared_access_required + + +@login_required +def share_view(request: HttpRequest, id: str, token: str): + """ Performs sharing of an ema + + If token given in url is not valid, the user will be redirected to the dashboard + + Args: + request (HttpRequest): The incoming request + id (str): EMA's id + token (str): Access token for EMA + + Returns: + + """ + user = request.user + obj = get_object_or_404(Ema, id=id) + # Check tokens + if obj.access_token == token: + # Send different messages in case user has already been added to list of sharing users + if obj.is_shared_with(user): + messages.info( + request, + _("{} has already been shared with you").format(obj.identifier) + ) + else: + messages.success( + request, + _("{} has been shared with you").format(obj.identifier) + ) + obj.share_with_user(user) + return redirect("ema:detail", id=id) + else: + messages.error( + request, + _("Share link invalid"), + extra_tags="danger", + ) + return redirect("home") + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def create_share_view(request: HttpRequest, id: str): + """ Renders sharing form for an Ema + + Args: + request (HttpRequest): The incoming request + id (str): Ema's id + + Returns: + + """ + obj = get_object_or_404(Ema, id=id) + form = ShareModalForm(request.POST or None, instance=obj, request=request) + return form.process_request( + request, + msg_success=_("Share settings updated") + ) diff --git a/ema/views/state.py b/ema/views/state.py new file mode 100644 index 0000000..1c7b0a4 --- /dev/null +++ b/ema/views/state.py @@ -0,0 +1,90 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +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.shortcuts import get_object_or_404 +from django.urls import reverse + +from compensation.forms.modals.state import NewCompensationStateModalForm, RemoveCompensationStateModalForm, \ + EditCompensationStateModalForm +from compensation.models import CompensationState +from ema.models import Ema +from konova.decorators import conservation_office_group_required, shared_access_required +from konova.utils.message_templates import COMPENSATION_STATE_ADDED, COMPENSATION_STATE_REMOVED, \ + COMPENSATION_STATE_EDITED + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def state_new_view(request: HttpRequest, id: str): + """ Renders a form for adding new states for an EMA + + Args: + request (HttpRequest): The incoming request + id (str): The EMA's id to which the new state will be related + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + form = NewCompensationStateModalForm(request.POST or None, instance=ema, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_ADDED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def state_remove_view(request: HttpRequest, id: str, state_id: str): + """ Renders a form for removing an EMA state + + Args: + request (HttpRequest): The incoming request + id (str): The ema id + state_id (str): The state's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + state = get_object_or_404(CompensationState, id=state_id) + form = RemoveCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_REMOVED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) + + +@login_required +@conservation_office_group_required +@shared_access_required(Ema, "id") +def state_edit_view(request: HttpRequest, id: str, state_id: str): + """ Renders a form for editing an EMA state + + Args: + request (HttpRequest): The incoming request + id (str): The ema id + state_id (str): The state's id + + Returns: + + """ + ema = get_object_or_404(Ema, id=id) + state = get_object_or_404(CompensationState, id=state_id) + form = EditCompensationStateModalForm(request.POST or None, instance=ema, state=state, request=request) + return form.process_request( + request, + msg_success=COMPENSATION_STATE_EDITED, + redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" + ) +