From 3842bcf0b14fe3cb8d3115be55affad0f96d7656 Mon Sep 17 00:00:00 2001 From: mipel Date: Wed, 6 Oct 2021 16:00:17 +0200 Subject: [PATCH] #7 New Form * adds NewEmaForm and EditEmaForm * refactors ResponsibilityData related form fields into reusable mixin CompensationResponsibleFormMixin * used in NewEcoAccountForm and NewEmaForm for easier maintaining and reducing amount of code * refactors templates /xy/new/view.html into /xy/form/view.html since the same template file is used for new and edit forms --- compensation/forms/forms.py | 105 +++++++----- .../compensation/{new => form}/view.html | 0 compensation/views/compensation_views.py | 5 +- compensation/views/eco_account_views.py | 5 +- ema/forms.py | 159 ++++++++++++++++++ .../ema/detail/includes/controls.html | 2 +- .../new => ema/templates/ema/form}/view.html | 0 ema/urls.py | 1 + ema/views.py | 89 +++++++++- .../templates/intervention/form/view.html | 6 + intervention/views.py | 5 +- 11 files changed, 319 insertions(+), 58 deletions(-) rename compensation/templates/compensation/{new => form}/view.html (100%) create mode 100644 ema/forms.py rename {intervention/templates/intervention/new => ema/templates/ema/form}/view.html (100%) create mode 100644 intervention/templates/intervention/form/view.html diff --git a/compensation/forms/forms.py b/compensation/forms/forms.py index 7c922f19..000cea2b 100644 --- a/compensation/forms/forms.py +++ b/compensation/forms/forms.py @@ -85,10 +85,61 @@ class AbstractCompensationForm(BaseForm): abstract = True +class CompensationResponsibleFormMixin(forms.Form): + """ Encapsulates form fields used in different compensation related models like EcoAccount or EMA + + """ + conservation_office = forms.ModelChoiceField( + label=_("Conservation office"), + label_suffix="", + help_text=_("Select the responsible office"), + queryset=KonovaCode.objects.filter( + is_archived=False, + is_leaf=True, + code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID], + ), + widget=autocomplete.ModelSelect2( + url="codes-conservation-office-autocomplete", + attrs={ + "data-placeholder": _("Click for selection") + } + ), + ) + conservation_file_number = forms.CharField( + label=_("Conservation office file number"), + label_suffix="", + max_length=255, + required=False, + widget=forms.TextInput( + attrs={ + "placeholder": _("ETS-123/ABC.456"), + "class": "form-control", + } + ) + ) + handler = forms.CharField( + label=_("Eco-account handler"), + label_suffix="", + max_length=255, + required=False, + help_text=_("Who handles the eco-account"), + widget=forms.TextInput( + attrs={ + "placeholder": _("Company Mustermann"), + "class": "form-control", + } + ) + ) + + class NewCompensationForm(AbstractCompensationForm): """ Form for creating new compensations. Can be initialized with an intervention id for preselecting the related intervention. + form = NewCompensationForm(request.POST or None, intervention_id=intervention_id) + ... + The intervention id will not be resolved into the intervention ORM object but instead will be used to initialize + the related form field. """ intervention = forms.ModelChoiceField( @@ -172,6 +223,9 @@ class NewCompensationForm(AbstractCompensationForm): class EditCompensationForm(NewCompensationForm): + """ Form for editing compensations + + """ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.form_title = _("Edit compensation") @@ -224,48 +278,12 @@ class EditCompensationForm(NewCompensationForm): return self.instance -class NewEcoAccountForm(AbstractCompensationForm): - conservation_office = forms.ModelChoiceField( - label=_("Conservation office"), - label_suffix="", - help_text=_("Select the responsible office"), - queryset=KonovaCode.objects.filter( - is_archived=False, - is_leaf=True, - code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID], - ), - widget=autocomplete.ModelSelect2( - url="codes-conservation-office-autocomplete", - attrs={ - "data-placeholder": _("Click for selection") - } - ), - ) - conservation_file_number = forms.CharField( - label=_("Conservation office file number"), - label_suffix="", - max_length=255, - required=False, - widget=forms.TextInput( - attrs={ - "placeholder": _("ETS-123/ABC.456"), - "class": "form-control", - } - ) - ) - handler = forms.CharField( - label=_("Eco-account handler"), - label_suffix="", - max_length=255, - required=False, - help_text=_("Who handles the eco-account"), - widget=forms.TextInput( - attrs={ - "placeholder": _("Company Mustermann"), - "class": "form-control", - } - ) - ) +class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin): + """ Form for creating eco accounts + + Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin + + """ field_order = [ "identifier", "title", @@ -333,6 +351,9 @@ class NewEcoAccountForm(AbstractCompensationForm): class EditEcoAccountForm(NewEcoAccountForm): + """ Form for editing eco accounts + + """ surface = forms.DecimalField( min_value=0.00, decimal_places=2, diff --git a/compensation/templates/compensation/new/view.html b/compensation/templates/compensation/form/view.html similarity index 100% rename from compensation/templates/compensation/new/view.html rename to compensation/templates/compensation/form/view.html diff --git a/compensation/views/compensation_views.py b/compensation/views/compensation_views.py index 4778e6d0..0d56d817 100644 --- a/compensation/views/compensation_views.py +++ b/compensation/views/compensation_views.py @@ -58,7 +58,7 @@ def new_view(request: HttpRequest, intervention_id: str = None): Returns: """ - template = "compensation/new/view.html" + template = "compensation/form/view.html" 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": @@ -83,7 +83,6 @@ def new_view(request: HttpRequest, intervention_id: str = None): context = { "form": data_form, "geom_form": geom_form, - "url": reverse("compensation:new-id") } context = BaseContext(request, context).context return render(request, template, context) @@ -119,7 +118,7 @@ def edit_view(request: HttpRequest, id: str): Returns: """ - template = "compensation/new/view.html" + template = "compensation/form/view.html" # Get object from db comp = get_object_or_404(Compensation, id=id) # Create forms, initialize with values from db/from POST request diff --git a/compensation/views/eco_account_views.py b/compensation/views/eco_account_views.py index c0cbc781..476bd977 100644 --- a/compensation/views/eco_account_views.py +++ b/compensation/views/eco_account_views.py @@ -68,7 +68,7 @@ def new_view(request: HttpRequest): Returns: """ - template = "compensation/new/view.html" + 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": @@ -93,7 +93,6 @@ def new_view(request: HttpRequest): context = { "form": data_form, "geom_form": geom_form, - "url": reverse("compensation:acc-new-id") } context = BaseContext(request, context).context return render(request, template, context) @@ -129,7 +128,7 @@ def edit_view(request: HttpRequest, id: str): Returns: """ - template = "compensation/new/view.html" + template = "compensation/form/view.html" # Get object from db acc = get_object_or_404(EcoAccount, id=id) # Create forms, initialize with values from db/from POST request diff --git a/ema/forms.py b/ema/forms.py new file mode 100644 index 00000000..d516259a --- /dev/null +++ b/ema/forms.py @@ -0,0 +1,159 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 06.10.21 + +""" +from django.contrib.auth.models import User +from django.db import transaction +from django.urls import reverse, reverse_lazy +from django.utils.translation import gettext_lazy as _ +from compensation.forms.forms import AbstractCompensationForm, CompensationResponsibleFormMixin +from ema.models import Ema +from intervention.models import ResponsibilityData +from konova.forms import SimpleGeomForm +from user.models import UserActionLogEntry, UserAction + + +class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin): + """ Form for creating new EMA objects. + + Inherits basic form fields from AbstractCompensationForm and additional from CompensationResponsibleFormMixin. + Second holds self.instance.response related fields + + """ + field_order = [ + "identifier", + "title", + "conservation_office", + "conservation_file_number", + "handler", + "fundings", + "comment", + ] + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("New EMA") + + self.action_url = reverse("ema:new") + self.cancel_redirect = reverse("ema:index") + + tmp = Ema() + identifier = tmp.generate_new_identifier() + self.initialize_form_field("identifier", identifier) + self.fields["identifier"].widget.attrs["url"] = reverse_lazy("ema:new-id") + self.fields["title"].widget.attrs["placeholder"] = _("Compensation XY; Location ABC") + + def save(self, user: User, geom_form: SimpleGeomForm): + with transaction.atomic(): + # Fetch data from cleaned POST values + identifier = self.cleaned_data.get("identifier", None) + title = self.cleaned_data.get("title", None) + fundings = self.cleaned_data.get("fundings", None) + handler = self.cleaned_data.get("handler", None) + conservation_office = self.cleaned_data.get("conservation_office", None) + conservation_file_number = self.cleaned_data.get("conservation_file_number", None) + comment = self.cleaned_data.get("comment", None) + + # Create log entry + action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.CREATED, + ) + # Process the geometry form + geometry = geom_form.save(action) + + responsible = ResponsibilityData.objects.create( + handler=handler, + conservation_file_number=conservation_file_number, + conservation_office=conservation_office, + ) + + # Finally create main object + acc = Ema.objects.create( + identifier=identifier, + title=title, + responsible=responsible, + created=action, + geometry=geometry, + comment=comment, + ) + acc.fundings.set(fundings) + + # Add the creating user to the list of shared users + acc.users.add(user) + + # Add the log entry to the main objects log list + acc.log.add(action) + return acc + + +class EditEmaForm(NewEmaForm): + """ Form for editing EMAs + + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Edit EMA") + + self.action_url = reverse("ema:edit", args=(self.instance.id,)) + self.cancel_redirect = reverse("ema:open", args=(self.instance.id,)) + + self.fields["identifier"].widget.attrs["url"] = reverse_lazy("ema:new-id") + self.fields["title"].widget.attrs["placeholder"] = _("Compensation XY; Location ABC") + + # Initialize form data + form_data = { + "identifier": self.instance.identifier, + "title": self.instance.title, + "handler": self.instance.responsible.handler, + "conservation_office": self.instance.responsible.conservation_office, + "conservation_file_number": self.instance.responsible.conservation_file_number, + "fundings": self.instance.fundings.all(), + "comment": self.instance.comment, + } + disabled_fields = [] + self.load_initial_data( + form_data, + disabled_fields + ) + + def save(self, user: User, geom_form: SimpleGeomForm): + with transaction.atomic(): + # Fetch data from cleaned POST values + identifier = self.cleaned_data.get("identifier", None) + title = self.cleaned_data.get("title", None) + fundings = self.cleaned_data.get("fundings", None) + handler = self.cleaned_data.get("handler", None) + conservation_office = self.cleaned_data.get("conservation_office", None) + conservation_file_number = self.cleaned_data.get("conservation_file_number", None) + comment = self.cleaned_data.get("comment", None) + + # Create log entry + action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.EDITED, + ) + # Process the geometry form + geometry = geom_form.save(action) + + # Update responsible data + self.instance.responsible.handler = handler + self.instance.responsible.conservation_office = conservation_office + self.instance.responsible.conservation_file_number = conservation_file_number + self.instance.responsible.save() + + # Update main oject data + self.instance.identifier = identifier + self.instance.title = title + self.instance.geometry = geometry + self.instance.comment = comment + self.instance.modified = action + self.instance.save() + self.instance.fundings.set(fundings) + + # Add the log entry to the main objects log list + self.instance.log.add(action) + return self.instance diff --git a/ema/templates/ema/detail/includes/controls.html b/ema/templates/ema/detail/includes/controls.html index 9dc960fa..8b24f66e 100644 --- a/ema/templates/ema/detail/includes/controls.html +++ b/ema/templates/ema/detail/includes/controls.html @@ -24,7 +24,7 @@ {% endif %} {% endif %} {% if is_default_member %} - + diff --git a/intervention/templates/intervention/new/view.html b/ema/templates/ema/form/view.html similarity index 100% rename from intervention/templates/intervention/new/view.html rename to ema/templates/ema/form/view.html diff --git a/ema/urls.py b/ema/urls.py index 860d863b..404cdf53 100644 --- a/ema/urls.py +++ b/ema/urls.py @@ -12,6 +12,7 @@ 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("", open_view, name="open"), path('/log', log_view, name='log'), path('/edit', edit_view, name='edit'), diff --git a/ema/views.py b/ema/views.py index 914c4129..e479c17b 100644 --- a/ema/views.py +++ b/ema/views.py @@ -1,12 +1,14 @@ +from django.contrib import messages from django.contrib.auth.decorators import login_required from django.db.models import Sum -from django.http import HttpRequest -from django.shortcuts import render, get_object_or_404 +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 _ import compensation from compensation.forms.modalForms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm +from ema.forms import NewEmaForm, EditEmaForm from ema.tables import EmaTable from konova.contexts import BaseContext from konova.decorators import conservation_office_group_required @@ -14,6 +16,7 @@ from ema.models import Ema, EmaDocument from konova.forms import RemoveModalForm, NewDocumentForm, SimpleGeomForm, RecordModalForm from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.utils.documents import get_document, remove_document +from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID from konova.utils.user_checks import in_group @@ -47,7 +50,8 @@ def index_view(request: HttpRequest): @login_required @conservation_office_group_required def new_view(request: HttpRequest): - """ Renders the form for a new EMA + """ + Renders a view for a new eco account creation Args: request (HttpRequest): The incoming request @@ -55,12 +59,54 @@ def new_view(request: HttpRequest): Returns: """ - template = "generic_index.html" - context = {} + 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:open", id=ema.id) + else: + messages.error(request, FORM_INVALID) + else: + # For clarification: nothing in this case + pass + context = { + "form": data_form, + "geom_form": geom_form, + } context = BaseContext(request, context).context return render(request, template, context) +@login_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={ + "identifier": identifier + } + ) + + @login_required def open_view(request: HttpRequest, id: str): """ Renders the detail view of an EMA @@ -133,7 +179,38 @@ def log_view(request: HttpRequest, id: str): @login_required def edit_view(request: HttpRequest, id: str): - get_object_or_404(Ema, id=id) + """ + 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) + # 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:open", id=ema.id) + else: + messages.error(request, FORM_INVALID) + else: + # For clarification: nothing in this case + pass + context = { + "form": data_form, + "geom_form": geom_form, + } + context = BaseContext(request, context).context + return render(request, template, context) @login_required diff --git a/intervention/templates/intervention/form/view.html b/intervention/templates/intervention/form/view.html new file mode 100644 index 00000000..eb37e6b7 --- /dev/null +++ b/intervention/templates/intervention/form/view.html @@ -0,0 +1,6 @@ +{% extends 'base.html' %} +{% load i18n l10n %} + +{% block body %} + {% include 'form/main_data_collapse_form.html' %} +{% endblock %} \ No newline at end of file diff --git a/intervention/views.py b/intervention/views.py index 5ef88eb0..8c9679f1 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -58,7 +58,7 @@ def new_view(request: HttpRequest): Returns: """ - template = "intervention/new/view.html" + 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": @@ -83,7 +83,6 @@ def new_view(request: HttpRequest): context = { "form": data_form, "geom_form": geom_form, - "url": reverse("intervention:new-id") } context = BaseContext(request, context).context return render(request, template, context) @@ -242,7 +241,7 @@ def edit_view(request: HttpRequest, id: str): Returns: """ - template = "intervention/new/view.html" + template = "intervention/form/view.html" # Get object from db intervention = get_object_or_404(Intervention, id=id) # Create forms, initialize with values from db/from POST request