From 877a1ca335b2fad3958402010d8eb86eebfb7a8e Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 27 Sep 2021 11:45:13 +0200 Subject: [PATCH] #7 New forms * refactors large forms.py into forms/modalForms.py and forms/forms.py * refactors custom input fields into intervention/inputs.py --- compensation/views/eco_account_views.py | 4 +- intervention/filters.py | 2 +- intervention/forms.py | 744 ------------------------ intervention/forms/forms.py | 362 ++++++++++++ intervention/forms/modalForms.py | 385 ++++++++++++ intervention/inputs.py | 15 + intervention/views.py | 14 +- 7 files changed, 772 insertions(+), 754 deletions(-) delete mode 100644 intervention/forms.py create mode 100644 intervention/forms/forms.py create mode 100644 intervention/forms/modalForms.py create mode 100644 intervention/inputs.py diff --git a/compensation/views/eco_account_views.py b/compensation/views/eco_account_views.py index c53f5349..9b008ba3 100644 --- a/compensation/views/eco_account_views.py +++ b/compensation/views/eco_account_views.py @@ -16,7 +16,7 @@ from django.shortcuts import render, get_object_or_404 from compensation.forms import NewStateModalForm, NewActionModalForm, NewDeadlineModalForm from compensation.models import EcoAccount, EcoAccountDocument from compensation.tables import EcoAccountTable -from intervention.forms import NewDeductionForm +from intervention.forms.modalForms import NewDeductionModalForm from konova.contexts import BaseContext from konova.decorators import any_group_check, default_group_required, conservation_office_group_required from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentForm, RecordModalForm @@ -340,7 +340,7 @@ def new_deduction_view(request: HttpRequest, id: str): """ acc = get_object_or_404(EcoAccount, id=id) - form = NewDeductionForm(request.POST or None, instance=acc, user=request.user) + form = NewDeductionModalForm(request.POST or None, instance=acc, user=request.user) return form.process_request( request, msg_success=_("Deduction added") diff --git a/intervention/filters.py b/intervention/filters.py index 319a0992..205b35c6 100644 --- a/intervention/filters.py +++ b/intervention/filters.py @@ -12,7 +12,7 @@ from django.db.models import QuerySet, Q from django.utils.translation import gettext_lazy as _ -from intervention.forms import DummyFilterInput +from intervention.inputs import DummyFilterInput from intervention.models import Intervention diff --git a/intervention/forms.py b/intervention/forms.py deleted file mode 100644 index 326986df..00000000 --- a/intervention/forms.py +++ /dev/null @@ -1,744 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 02.12.20 - -""" -from dal import autocomplete -from django import forms -from django.contrib.auth.models import User -from django.contrib.gis.geos import Polygon -from django.db import transaction -from django.urls import reverse -from django.utils import timezone -from django.utils.translation import gettext_lazy as _ - -from codelist.models import KonovaCode -from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \ - CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID -from compensation.models import EcoAccountDeduction, EcoAccount -from intervention.models import Intervention, Revocation, RevocationDocument, LegalData, ResponsibilityData -from konova.forms import BaseForm, BaseModalForm, SimpleGeomForm -from konova.settings import ZB_GROUP, ETS_GROUP -from konova.utils.general import format_german_float -from konova.utils.messenger import Messenger -from konova.utils.user_checks import in_group -from user.models import UserActionLogEntry, UserAction - - -class NewInterventionForm(BaseForm): - identifier = forms.CharField( - label=_("Identifier"), - label_suffix="", - max_length=255, - help_text=_("Generated automatically"), - ) - title = forms.CharField( - label=_("Title"), - label_suffix="", - help_text=_("An explanatory name"), - max_length=255, - widget=forms.TextInput( - attrs={ - "placeholder": _("Construction XY; Location ABC") - } - ) - ) - type = forms.ModelChoiceField( - label=_("Process type"), - label_suffix="", - help_text=_(""), - required=False, - queryset=KonovaCode.objects.filter( - is_archived=False, - is_leaf=True, - code_lists__in=[CODELIST_PROCESS_TYPE_ID], - ), - widget=autocomplete.ModelSelect2( - url="codes-process-type-autocomplete", - attrs={ - } - ), - ) - laws = forms.ModelMultipleChoiceField( - label=_("Law"), - label_suffix="", - help_text=_("Multiple selection possible"), - required=False, - queryset=KonovaCode.objects.filter( - is_archived=False, - is_leaf=True, - code_lists__in=[CODELIST_LAW_ID], - ), - widget=autocomplete.ModelSelect2Multiple( - url="codes-law-autocomplete", - attrs={ - } - ), - ) - registration_office = forms.ModelChoiceField( - label=_("Registration office"), - label_suffix="", - required=False, - queryset=KonovaCode.objects.filter( - is_archived=False, - is_leaf=True, - code_lists__in=[CODELIST_REGISTRATION_OFFICE_ID], - ), - widget=autocomplete.ModelSelect2( - url="codes-registration-office-autocomplete", - attrs={ - } - ), - ) - conservation_office = forms.ModelChoiceField( - label=_("Conservation office"), - label_suffix="", - required=False, - 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={ - } - ), - ) - registration_file_number = forms.CharField( - label=_("Registration office file number"), - label_suffix="", - max_length=255, - required=False, - widget=forms.TextInput( - attrs={ - "placeholder": _("ZB-123/ABC.456") - } - ) - ) - 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") - } - ) - ) - handler = forms.CharField( - label=_("Intervention handler"), - label_suffix="", - max_length=255, - required=False, - help_text=_("Who performs the intervention"), - widget=forms.TextInput( - attrs={ - "placeholder": _("Company Mustermann") - } - ) - ) - registration_date = forms.DateField( - label=_("Registration date"), - label_suffix=_(""), - required=False, - widget=forms.DateInput( - attrs={ - "type": "date", - }, - format="%d.%m.%Y" - ) - ) - binding_date = forms.DateField( - label=_("Binding on"), - label_suffix=_(""), - required=False, - widget=forms.DateInput( - attrs={ - "type": "date", - }, - format="%d.%m.%Y" - ) - ) - comment = forms.CharField( - label_suffix="", - label=_("Comment"), - required=False, - help_text=_("Additional comment"), - widget=forms.Textarea( - attrs={ - "rows": 5, - "class": "w-100" - } - ) - ) - - # Define w-100 for all form fields - full_width_fields = True - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("New intervention") - self.action_url = reverse("intervention:new") - self.cancel_redirect = reverse("intervention:index") - - tmp_intervention = Intervention() - identifier = tmp_intervention._generate_new_identifier() - self.initialize_form_field("identifier", identifier) - - 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) - _type = self.cleaned_data.get("type", None) - laws = self.cleaned_data.get("laws", None) - handler = self.cleaned_data.get("handler", None) - registration_office = self.cleaned_data.get("registration_office", None) - conservation_office = self.cleaned_data.get("conservation_office", None) - conservation_file_number = self.cleaned_data.get("conservation_file_number", None) - registration_file_number = self.cleaned_data.get("registration_file_number", None) - binding_date = self.cleaned_data.get("binding_date", None) - registration_date = self.cleaned_data.get("registration_date", None) - comment = self.cleaned_data.get("comment", None) - - # Create log entry - action = UserActionLogEntry.objects.create( - user=user, - action=UserAction.CREATED, - ) - - # Create legal data object (without M2M laws first) - legal_data = LegalData.objects.create( - registration_date=registration_date, - binding_date=binding_date, - process_type=_type, - ) - # Then add the M2M laws to the object - legal_data.laws.set(laws) - - # Create responsible data object - responsibility_data = ResponsibilityData.objects.create( - registration_office=registration_office, - conservation_office=conservation_office, - registration_file_number=registration_file_number, - conservation_file_number=conservation_file_number, - handler=handler, - ) - - # Process the geometry form - geometry = geom_form.save(action) - - # Finally create main object, holding the other objects - intervention = Intervention.objects.create( - identifier=identifier, - title=title, - responsible=responsibility_data, - legal=legal_data, - created=action, - geometry=geometry, - comment=comment, - ) - - # Add the log entry to the main objects log list - intervention.log.add(action) - - # Add the performing user as the first user having access to the data - intervention.users.add(user) - return intervention - - -class EditInterventionForm(NewInterventionForm): - """ Subclasses NewInterventionForm - - Simply adds initializing of a provided self.instance object into the form fields - - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - if self.instance is not None: - self.action_url = reverse("intervention:edit", args=(self.instance.id,)) - self.cancel_redirect = reverse("intervention:open", args=(self.instance.id,)) - self.form_title = _("Edit intervention") - self.form_caption = "" - - reg_date = self.instance.legal.registration_date - bind_date = self.instance.legal.binding_date - if reg_date is not None: - reg_date = reg_date.isoformat() - if bind_date is not None: - bind_date = bind_date.isoformat() - - # Initialize form data - form_data = { - "identifier": self.instance.identifier, - "title": self.instance.title, - "type": self.instance.legal.process_type, - "laws": list(self.instance.legal.laws.values_list("id", flat=True)), - "handler": self.instance.responsible.handler, - "registration_office": self.instance.responsible.registration_office, - "registration_file_number": self.instance.responsible.registration_file_number, - "conservation_office": self.instance.responsible.conservation_office, - "conservation_file_number": self.instance.responsible.conservation_file_number, - "registration_date": reg_date, - "binding_date": bind_date, - "comment": self.instance.comment, - } - disabled_fields = [ - "identifier", - ] - self.load_initial_data( - form_data, - disabled_fields, - ) - - def save(self, user: User, geom_form: SimpleGeomForm): - """ Overwrite instance with new form data - - Args: - user (): - - Returns: - - """ - with transaction.atomic(): - identifier = self.cleaned_data.get("identifier", None) - title = self.cleaned_data.get("title", None) - process_type = self.cleaned_data.get("type", None) - laws = self.cleaned_data.get("laws", None) - handler = self.cleaned_data.get("handler", None) - registration_office = self.cleaned_data.get("registration_office", None) - registration_file_number = self.cleaned_data.get("registration_file_number", None) - conservation_office = self.cleaned_data.get("conservation_office", None) - conservation_file_number = self.cleaned_data.get("conservation_file_number", None) - registration_date = self.cleaned_data.get("registration_date", None) - binding_date = self.cleaned_data.get("binding_date", None) - comment = self.cleaned_data.get("comment", None) - - self.instance.legal.process_type = process_type - self.instance.legal.registration_date = registration_date - self.instance.legal.binding_date = binding_date - self.instance.legal.laws.set(laws) - self.instance.legal.save() - - self.instance.responsible.handler = handler - self.instance.responsible.registration_office = registration_office - self.instance.responsible.registration_file_number = registration_file_number - self.instance.responsible.conservation_office = conservation_office - self.instance.responsible.conservation_file_number = conservation_file_number - self.instance.responsible.save() - - user_action = UserActionLogEntry.objects.create( - user=user, - timestamp=timezone.now(), - action=UserAction.EDITED, - ) - - geometry = geom_form.save(user_action) - self.instance.geometry = geometry - self.instance.geometry.save() - - self.instance.log.add(user_action) - - self.instance.identifier = identifier - self.instance.title = title - self.instance.comment = comment - self.instance.modified = user_action - self.instance.save() - - return self.instance - - -class OpenInterventionForm(EditInterventionForm): - """ - This form is not intended to be used as data-input form. It's used to simplify the rendering of intervention:open - """ - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - - # Resize map - self.fields["geometry"].widget.attrs["map_width"] = 500 - self.fields["geometry"].widget.attrs["map_height"] = 300 - - # Disable all form fields - for field in self.fields: - self.disable_form_field(field) - - -class DummyFilterInput(forms.HiddenInput): - """ A dummy input widget - - Does not render anything. Can be used to keep filter logic using django_filter without having a pre defined - filter widget being rendered to the template. - - """ - template_name = "konova/custom_widgets/dummy-filter-input.html" - - -class TextToClipboardInput(forms.TextInput): - template_name = "konova/custom_widgets/text-to-clipboard-input.html" - - -class ShareInterventionForm(BaseModalForm): - url = forms.CharField( - label=_("Share link"), - label_suffix="", - help_text=_("Send this link to users who you want to have writing access on the data"), - required=False, - widget=TextToClipboardInput( - attrs={ - "readonly": True - } - ) - ) - users = forms.MultipleChoiceField( - label=_("Shared with"), - label_suffix="", - required=True, - help_text=_("Remove check to remove access for this user"), - widget=forms.CheckboxSelectMultiple( - attrs={ - "class": "list-unstyled", - } - ), - choices=[] - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Share") - self.form_caption = _("Share settings for {}").format(self.instance.identifier) - self.template = "modal/modal_form.html" - - # Make sure an access_token is set - if self.instance.access_token is None: - self.instance.generate_access_token() - - self._init_fields() - - def _init_fields(self): - """ Wraps initializing of fields - - Returns: - - """ - # Initialize share_link field - self.share_link = self.request.build_absolute_uri( - reverse("intervention:share", args=(self.instance.id, self.instance.access_token,)) - ) - self.initialize_form_field( - "url", - self.share_link - ) - - # Initialize users field - # Remove field if user is not in registration or conservation group - if not in_group(self.request.user, ZB_GROUP) and not in_group(self.request.user, ETS_GROUP): - del self.fields["users"] - else: - users = self.instance.users.all() - choices = [] - for n in users: - choices.append( - (n.id, n.username) - ) - self.fields["users"].choices = choices - u_ids = list(users.values_list("id", flat=True)) - self.initialize_form_field( - "users", - u_ids - ) - - def save(self): - accessing_users = User.objects.filter( - id__in=self.cleaned_data["users"] - ) - self.instance.users.set(accessing_users) - - -class NewRevocationForm(BaseModalForm): - date = forms.DateField( - label=_("Date"), - label_suffix=_(""), - help_text=_("Date of revocation"), - widget=forms.DateInput( - attrs={ - "type": "date", - "data-provide": "datepicker", - }, - format="%d.%m.%Y" - ) - ) - file = forms.FileField( - label=_("Document"), - label_suffix=_(""), - required=False, - help_text=_("Must be smaller than 15 Mb"), - widget=forms.FileInput( - attrs={ - "class": "w-75" - } - ) - ) - comment = forms.CharField( - required=False, - max_length=200, - label=_("Comment"), - label_suffix=_(""), - help_text=_("Additional comment, maximum {} letters").format(200), - widget=forms.Textarea( - attrs={ - "cols": 30, - "rows": 5, - } - ) - ) - - # Define w-100 for all form fields - full_width_fields = True - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Add revocation") - self.form_caption = "" - self.form_attrs = { - "enctype": "multipart/form-data", # important for file upload - } - - def save(self): - with transaction.atomic(): - created_action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.CREATED - ) - edited_action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.EDITED - ) - revocation = Revocation.objects.create( - date=self.cleaned_data["date"], - comment=self.cleaned_data["comment"], - created=created_action, - ) - self.instance.modified = edited_action - self.instance.save() - self.instance.log.add(edited_action) - self.instance.legal.revocation = revocation - self.instance.legal.save() - - if self.cleaned_data["file"]: - RevocationDocument.objects.create( - title="revocation_of_{}".format(self.instance.identifier), - date_of_creation=self.cleaned_data["date"], - comment=self.cleaned_data["comment"], - file=self.cleaned_data["file"], - instance=revocation - ) - return revocation - - -class RunCheckForm(BaseModalForm): - checked_intervention = forms.BooleanField( - label=_("Checked intervention data"), - label_suffix="", - widget=forms.CheckboxInput(), - required=True, - ) - checked_comps = forms.BooleanField( - label=_("Checked compensations data and payments"), - label_suffix="", - widget=forms.CheckboxInput(), - required=True - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Run check") - self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) - - def is_valid(self) -> bool: - """ Perform a validity check based on quality_check() logic - - Returns: - result (bool) - """ - super_result = super().is_valid() - # Perform check - msgs = self.instance.quality_check() - for msg in msgs: - self.add_error( - "checked_intervention", - msg - ) - return super_result and (len(msgs) == 0) - - def save(self): - """ Saving logic - - Returns: - - """ - with transaction.atomic(): - user_action = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.CHECKED - ) - # Replace old checked - if self.instance.checked: - self.instance.checked.delete() - self.instance.checked = user_action - self.instance.log.add(user_action) - self.instance.save() - - # Send message to the SSO server - messenger = Messenger( - self.instance.users.all(), - type="INFO", - ) - messenger.send_object_checked( - self.instance.identifier, - self.user, - ) - - -class NewDeductionForm(BaseModalForm): - """ Form for creating new deduction - - Can be used for Intervention view as well as for EcoAccount views. - - Parameter 'instance' can be an intervention, as well as an ecoAccount. - An instance check handles both workflows properly. - - """ - account = forms.ModelChoiceField( - label=_("Eco-account"), - label_suffix="", - help_text=_("Only recorded accounts can be selected for deductions"), - queryset=EcoAccount.objects.filter(deleted=None), - widget=autocomplete.ModelSelect2( - url="accounts-autocomplete", - attrs={ - "data-placeholder": _("Eco-account"), - "data-minimum-input-length": 3, - "readonly": True, - } - ), - ) - surface = forms.DecimalField( - min_value=0.00, - decimal_places=2, - label=_("Surface"), - label_suffix="", - help_text=_("in m²"), - ) - intervention = forms.ModelChoiceField( - label=_("Intervention"), - label_suffix="", - help_text=_("Only shared interventions can be selected"), - queryset=Intervention.objects.filter(deleted=None), - widget=autocomplete.ModelSelect2( - url="interventions-autocomplete", - attrs={ - "data-placeholder": _("Intervention"), - "data-minimum-input-length": 3, - } - ), - ) - - # Define w-100 for all form fields - full_width_fields = True - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("New Deduction") - self.form_caption = _("Enter the information for a new deduction from a chosen eco-account") - self.is_intervention_initially = False - - # Add a placeholder for field 'surface' without having to define the whole widget above - self.add_placeholder_for_field("surface", "0,00") - - # Check for Intervention or EcoAccount - if isinstance(self.instance, Intervention): - # Form has been called with a given intervention - self.initialize_form_field("intervention", self.instance) - self.disable_form_field("intervention") - self.is_intervention_initially = True - elif isinstance(self.instance, EcoAccount): - # Form has been called with a given account --> make it initial in the form and read-only - self.initialize_form_field("account", self.instance) - self.disable_form_field("account") - else: - raise NotImplementedError - - def is_valid(self): - """ Custom validity check - - Makes sure the deduction can not contain more surface than the account still provides - - Returns: - is_valid (bool) - """ - super_result = super().is_valid() - if self.is_intervention_initially: - acc = self.cleaned_data["account"] - else: - acc = self.instance - - if not acc.recorded: - self.add_error( - "account", - _("Eco-account {} is not recorded yet. You can only deduct from recorded accounts.").format(acc.identifier) - ) - return False - - # Calculate valid surface - sum_surface = acc.get_surface() - sum_surface_deductions = acc.get_deductions_surface() - rest_surface = sum_surface - sum_surface_deductions - form_surface = float(self.cleaned_data["surface"]) - is_valid_surface = form_surface < rest_surface - if not is_valid_surface: - self.add_error( - "surface", - _("The account {} has not enough surface for a deduction of {} m². There are only {} m² left").format( - acc.identifier, - format_german_float(form_surface), - format_german_float(rest_surface), - ), - ) - return is_valid_surface and super_result - - def save(self): - with transaction.atomic(): - # Create log entry - user_action_edit = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.EDITED - ) - user_action_create = UserActionLogEntry.objects.create( - user=self.user, - action=UserAction.CREATED - ) - self.instance.log.add(user_action_edit) - self.instance.modified = user_action_edit - self.instance.save() - - # Create deductions depending on Intervention or EcoAccount as the initial instance - if self.is_intervention_initially: - deduction = EcoAccountDeduction.objects.create( - intervention=self.instance, - account=self.cleaned_data["account"], - surface=self.cleaned_data["surface"], - created=user_action_create, - ) - else: - deduction = EcoAccountDeduction.objects.create( - intervention=self.cleaned_data["intervention"], - account=self.instance, - surface=self.cleaned_data["surface"], - created=user_action_create, - ) - return deduction \ No newline at end of file diff --git a/intervention/forms/forms.py b/intervention/forms/forms.py new file mode 100644 index 00000000..b6252453 --- /dev/null +++ b/intervention/forms/forms.py @@ -0,0 +1,362 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 02.12.20 + +""" +from dal import autocomplete +from django import forms +from django.contrib.auth.models import User +from django.db import transaction +from django.urls import reverse +from django.utils import timezone +from django.utils.translation import gettext_lazy as _ + +from codelist.models import KonovaCode +from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \ + CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID +from intervention.models import Intervention, LegalData, ResponsibilityData +from konova.forms import BaseForm, SimpleGeomForm +from user.models import UserActionLogEntry, UserAction + + +class NewInterventionForm(BaseForm): + identifier = forms.CharField( + label=_("Identifier"), + label_suffix="", + max_length=255, + help_text=_("Generated automatically"), + ) + title = forms.CharField( + label=_("Title"), + label_suffix="", + help_text=_("An explanatory name"), + max_length=255, + widget=forms.TextInput( + attrs={ + "placeholder": _("Construction XY; Location ABC") + } + ) + ) + type = forms.ModelChoiceField( + label=_("Process type"), + label_suffix="", + help_text=_(""), + required=False, + queryset=KonovaCode.objects.filter( + is_archived=False, + is_leaf=True, + code_lists__in=[CODELIST_PROCESS_TYPE_ID], + ), + widget=autocomplete.ModelSelect2( + url="codes-process-type-autocomplete", + attrs={ + } + ), + ) + laws = forms.ModelMultipleChoiceField( + label=_("Law"), + label_suffix="", + help_text=_("Multiple selection possible"), + required=False, + queryset=KonovaCode.objects.filter( + is_archived=False, + is_leaf=True, + code_lists__in=[CODELIST_LAW_ID], + ), + widget=autocomplete.ModelSelect2Multiple( + url="codes-law-autocomplete", + attrs={ + } + ), + ) + registration_office = forms.ModelChoiceField( + label=_("Registration office"), + label_suffix="", + required=False, + queryset=KonovaCode.objects.filter( + is_archived=False, + is_leaf=True, + code_lists__in=[CODELIST_REGISTRATION_OFFICE_ID], + ), + widget=autocomplete.ModelSelect2( + url="codes-registration-office-autocomplete", + attrs={ + } + ), + ) + conservation_office = forms.ModelChoiceField( + label=_("Conservation office"), + label_suffix="", + required=False, + 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={ + } + ), + ) + registration_file_number = forms.CharField( + label=_("Registration office file number"), + label_suffix="", + max_length=255, + required=False, + widget=forms.TextInput( + attrs={ + "placeholder": _("ZB-123/ABC.456") + } + ) + ) + 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") + } + ) + ) + handler = forms.CharField( + label=_("Intervention handler"), + label_suffix="", + max_length=255, + required=False, + help_text=_("Who performs the intervention"), + widget=forms.TextInput( + attrs={ + "placeholder": _("Company Mustermann") + } + ) + ) + registration_date = forms.DateField( + label=_("Registration date"), + label_suffix=_(""), + required=False, + widget=forms.DateInput( + attrs={ + "type": "date", + }, + format="%d.%m.%Y" + ) + ) + binding_date = forms.DateField( + label=_("Binding on"), + label_suffix=_(""), + required=False, + widget=forms.DateInput( + attrs={ + "type": "date", + }, + format="%d.%m.%Y" + ) + ) + comment = forms.CharField( + label_suffix="", + label=_("Comment"), + required=False, + help_text=_("Additional comment"), + widget=forms.Textarea( + attrs={ + "rows": 5, + "class": "w-100" + } + ) + ) + + # Define w-100 for all form fields + full_width_fields = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("New intervention") + self.action_url = reverse("intervention:new") + self.cancel_redirect = reverse("intervention:index") + + tmp_intervention = Intervention() + identifier = tmp_intervention._generate_new_identifier() + self.initialize_form_field("identifier", identifier) + + 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) + _type = self.cleaned_data.get("type", None) + laws = self.cleaned_data.get("laws", None) + handler = self.cleaned_data.get("handler", None) + registration_office = self.cleaned_data.get("registration_office", None) + conservation_office = self.cleaned_data.get("conservation_office", None) + conservation_file_number = self.cleaned_data.get("conservation_file_number", None) + registration_file_number = self.cleaned_data.get("registration_file_number", None) + binding_date = self.cleaned_data.get("binding_date", None) + registration_date = self.cleaned_data.get("registration_date", None) + comment = self.cleaned_data.get("comment", None) + + # Create log entry + action = UserActionLogEntry.objects.create( + user=user, + action=UserAction.CREATED, + ) + + # Create legal data object (without M2M laws first) + legal_data = LegalData.objects.create( + registration_date=registration_date, + binding_date=binding_date, + process_type=_type, + ) + # Then add the M2M laws to the object + legal_data.laws.set(laws) + + # Create responsible data object + responsibility_data = ResponsibilityData.objects.create( + registration_office=registration_office, + conservation_office=conservation_office, + registration_file_number=registration_file_number, + conservation_file_number=conservation_file_number, + handler=handler, + ) + + # Process the geometry form + geometry = geom_form.save(action) + + # Finally create main object, holding the other objects + intervention = Intervention.objects.create( + identifier=identifier, + title=title, + responsible=responsibility_data, + legal=legal_data, + created=action, + geometry=geometry, + comment=comment, + ) + + # Add the log entry to the main objects log list + intervention.log.add(action) + + # Add the performing user as the first user having access to the data + intervention.users.add(user) + return intervention + + +class EditInterventionForm(NewInterventionForm): + """ Subclasses NewInterventionForm + + Simply adds initializing of a provided self.instance object into the form fields + + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + if self.instance is not None: + self.action_url = reverse("intervention:edit", args=(self.instance.id,)) + self.cancel_redirect = reverse("intervention:open", args=(self.instance.id,)) + self.form_title = _("Edit intervention") + self.form_caption = "" + + reg_date = self.instance.legal.registration_date + bind_date = self.instance.legal.binding_date + if reg_date is not None: + reg_date = reg_date.isoformat() + if bind_date is not None: + bind_date = bind_date.isoformat() + + # Initialize form data + form_data = { + "identifier": self.instance.identifier, + "title": self.instance.title, + "type": self.instance.legal.process_type, + "laws": list(self.instance.legal.laws.values_list("id", flat=True)), + "handler": self.instance.responsible.handler, + "registration_office": self.instance.responsible.registration_office, + "registration_file_number": self.instance.responsible.registration_file_number, + "conservation_office": self.instance.responsible.conservation_office, + "conservation_file_number": self.instance.responsible.conservation_file_number, + "registration_date": reg_date, + "binding_date": bind_date, + "comment": self.instance.comment, + } + disabled_fields = [ + "identifier", + ] + self.load_initial_data( + form_data, + disabled_fields, + ) + + def save(self, user: User, geom_form: SimpleGeomForm): + """ Overwrite instance with new form data + + Args: + user (): + + Returns: + + """ + with transaction.atomic(): + identifier = self.cleaned_data.get("identifier", None) + title = self.cleaned_data.get("title", None) + process_type = self.cleaned_data.get("type", None) + laws = self.cleaned_data.get("laws", None) + handler = self.cleaned_data.get("handler", None) + registration_office = self.cleaned_data.get("registration_office", None) + registration_file_number = self.cleaned_data.get("registration_file_number", None) + conservation_office = self.cleaned_data.get("conservation_office", None) + conservation_file_number = self.cleaned_data.get("conservation_file_number", None) + registration_date = self.cleaned_data.get("registration_date", None) + binding_date = self.cleaned_data.get("binding_date", None) + comment = self.cleaned_data.get("comment", None) + + self.instance.legal.process_type = process_type + self.instance.legal.registration_date = registration_date + self.instance.legal.binding_date = binding_date + self.instance.legal.laws.set(laws) + self.instance.legal.save() + + self.instance.responsible.handler = handler + self.instance.responsible.registration_office = registration_office + self.instance.responsible.registration_file_number = registration_file_number + self.instance.responsible.conservation_office = conservation_office + self.instance.responsible.conservation_file_number = conservation_file_number + self.instance.responsible.save() + + user_action = UserActionLogEntry.objects.create( + user=user, + timestamp=timezone.now(), + action=UserAction.EDITED, + ) + + geometry = geom_form.save(user_action) + self.instance.geometry = geometry + self.instance.geometry.save() + + self.instance.log.add(user_action) + + self.instance.identifier = identifier + self.instance.title = title + self.instance.comment = comment + self.instance.modified = user_action + self.instance.save() + + return self.instance + + +class OpenInterventionForm(EditInterventionForm): + """ + This form is not intended to be used as data-input form. It's used to simplify the rendering of intervention:open + """ + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + + # Resize map + self.fields["geometry"].widget.attrs["map_width"] = 500 + self.fields["geometry"].widget.attrs["map_height"] = 300 + + # Disable all form fields + for field in self.fields: + self.disable_form_field(field) diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py new file mode 100644 index 00000000..1165a806 --- /dev/null +++ b/intervention/forms/modalForms.py @@ -0,0 +1,385 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 27.09.21 + +""" +from dal import autocomplete +from django.contrib.auth.models import User +from django.db import transaction +from django import forms +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from compensation.models import EcoAccount, EcoAccountDeduction +from intervention.inputs import TextToClipboardInput +from intervention.models import Revocation, RevocationDocument, Intervention +from konova.forms import BaseModalForm +from konova.settings import ZB_GROUP, ETS_GROUP +from konova.utils.general import format_german_float +from konova.utils.messenger import Messenger +from konova.utils.user_checks import in_group +from user.models import UserActionLogEntry, UserAction + + +class ShareInterventionModalForm(BaseModalForm): + url = forms.CharField( + label=_("Share link"), + label_suffix="", + help_text=_("Send this link to users who you want to have writing access on the data"), + required=False, + widget=TextToClipboardInput( + attrs={ + "readonly": True + } + ) + ) + users = forms.MultipleChoiceField( + label=_("Shared with"), + label_suffix="", + required=True, + help_text=_("Remove check to remove access for this user"), + widget=forms.CheckboxSelectMultiple( + attrs={ + "class": "list-unstyled", + } + ), + choices=[] + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Share") + self.form_caption = _("Share settings for {}").format(self.instance.identifier) + self.template = "modal/modal_form.html" + + # Make sure an access_token is set + if self.instance.access_token is None: + self.instance.generate_access_token() + + self._init_fields() + + def _init_fields(self): + """ Wraps initializing of fields + + Returns: + + """ + # Initialize share_link field + self.share_link = self.request.build_absolute_uri( + reverse("intervention:share", args=(self.instance.id, self.instance.access_token,)) + ) + self.initialize_form_field( + "url", + self.share_link + ) + + # Initialize users field + # Remove field if user is not in registration or conservation group + if not in_group(self.request.user, ZB_GROUP) and not in_group(self.request.user, ETS_GROUP): + del self.fields["users"] + else: + users = self.instance.users.all() + choices = [] + for n in users: + choices.append( + (n.id, n.username) + ) + self.fields["users"].choices = choices + u_ids = list(users.values_list("id", flat=True)) + self.initialize_form_field( + "users", + u_ids + ) + + def save(self): + accessing_users = User.objects.filter( + id__in=self.cleaned_data["users"] + ) + self.instance.users.set(accessing_users) + + +class NewRevocationModalForm(BaseModalForm): + date = forms.DateField( + label=_("Date"), + label_suffix=_(""), + help_text=_("Date of revocation"), + widget=forms.DateInput( + attrs={ + "type": "date", + "data-provide": "datepicker", + }, + format="%d.%m.%Y" + ) + ) + file = forms.FileField( + label=_("Document"), + label_suffix=_(""), + required=False, + help_text=_("Must be smaller than 15 Mb"), + widget=forms.FileInput( + attrs={ + "class": "w-75" + } + ) + ) + comment = forms.CharField( + required=False, + max_length=200, + label=_("Comment"), + label_suffix=_(""), + help_text=_("Additional comment, maximum {} letters").format(200), + widget=forms.Textarea( + attrs={ + "cols": 30, + "rows": 5, + } + ) + ) + + # Define w-100 for all form fields + full_width_fields = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Add revocation") + self.form_caption = "" + self.form_attrs = { + "enctype": "multipart/form-data", # important for file upload + } + + def save(self): + with transaction.atomic(): + created_action = UserActionLogEntry.objects.create( + user=self.user, + action=UserAction.CREATED + ) + edited_action = UserActionLogEntry.objects.create( + user=self.user, + action=UserAction.EDITED + ) + revocation = Revocation.objects.create( + date=self.cleaned_data["date"], + comment=self.cleaned_data["comment"], + created=created_action, + ) + self.instance.modified = edited_action + self.instance.save() + self.instance.log.add(edited_action) + self.instance.legal.revocation = revocation + self.instance.legal.save() + + if self.cleaned_data["file"]: + RevocationDocument.objects.create( + title="revocation_of_{}".format(self.instance.identifier), + date_of_creation=self.cleaned_data["date"], + comment=self.cleaned_data["comment"], + file=self.cleaned_data["file"], + instance=revocation + ) + return revocation + + +class RunCheckModalForm(BaseModalForm): + checked_intervention = forms.BooleanField( + label=_("Checked intervention data"), + label_suffix="", + widget=forms.CheckboxInput(), + required=True, + ) + checked_comps = forms.BooleanField( + label=_("Checked compensations data and payments"), + label_suffix="", + widget=forms.CheckboxInput(), + required=True + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Run check") + self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) + + def is_valid(self) -> bool: + """ Perform a validity check based on quality_check() logic + + Returns: + result (bool) + """ + super_result = super().is_valid() + # Perform check + msgs = self.instance.quality_check() + for msg in msgs: + self.add_error( + "checked_intervention", + msg + ) + return super_result and (len(msgs) == 0) + + def save(self): + """ Saving logic + + Returns: + + """ + with transaction.atomic(): + user_action = UserActionLogEntry.objects.create( + user=self.user, + action=UserAction.CHECKED + ) + # Replace old checked + if self.instance.checked: + self.instance.checked.delete() + self.instance.checked = user_action + self.instance.log.add(user_action) + self.instance.save() + + # Send message to the SSO server + messenger = Messenger( + self.instance.users.all(), + type="INFO", + ) + messenger.send_object_checked( + self.instance.identifier, + self.user, + ) + + +class NewDeductionModalForm(BaseModalForm): + """ Form for creating new deduction + + Can be used for Intervention view as well as for EcoAccount views. + + Parameter 'instance' can be an intervention, as well as an ecoAccount. + An instance check handles both workflows properly. + + """ + account = forms.ModelChoiceField( + label=_("Eco-account"), + label_suffix="", + help_text=_("Only recorded accounts can be selected for deductions"), + queryset=EcoAccount.objects.filter(deleted=None), + widget=autocomplete.ModelSelect2( + url="accounts-autocomplete", + attrs={ + "data-placeholder": _("Eco-account"), + "data-minimum-input-length": 3, + "readonly": True, + } + ), + ) + surface = forms.DecimalField( + min_value=0.00, + decimal_places=2, + label=_("Surface"), + label_suffix="", + help_text=_("in m²"), + ) + intervention = forms.ModelChoiceField( + label=_("Intervention"), + label_suffix="", + help_text=_("Only shared interventions can be selected"), + queryset=Intervention.objects.filter(deleted=None), + widget=autocomplete.ModelSelect2( + url="interventions-autocomplete", + attrs={ + "data-placeholder": _("Intervention"), + "data-minimum-input-length": 3, + } + ), + ) + + # Define w-100 for all form fields + full_width_fields = True + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("New Deduction") + self.form_caption = _("Enter the information for a new deduction from a chosen eco-account") + self.is_intervention_initially = False + + # Add a placeholder for field 'surface' without having to define the whole widget above + self.add_placeholder_for_field("surface", "0,00") + + # Check for Intervention or EcoAccount + if isinstance(self.instance, Intervention): + # Form has been called with a given intervention + self.initialize_form_field("intervention", self.instance) + self.disable_form_field("intervention") + self.is_intervention_initially = True + elif isinstance(self.instance, EcoAccount): + # Form has been called with a given account --> make it initial in the form and read-only + self.initialize_form_field("account", self.instance) + self.disable_form_field("account") + else: + raise NotImplementedError + + def is_valid(self): + """ Custom validity check + + Makes sure the deduction can not contain more surface than the account still provides + + Returns: + is_valid (bool) + """ + super_result = super().is_valid() + if self.is_intervention_initially: + acc = self.cleaned_data["account"] + else: + acc = self.instance + + if not acc.recorded: + self.add_error( + "account", + _("Eco-account {} is not recorded yet. You can only deduct from recorded accounts.").format(acc.identifier) + ) + return False + + # Calculate valid surface + sum_surface = acc.get_surface() + sum_surface_deductions = acc.get_deductions_surface() + rest_surface = sum_surface - sum_surface_deductions + form_surface = float(self.cleaned_data["surface"]) + is_valid_surface = form_surface < rest_surface + if not is_valid_surface: + self.add_error( + "surface", + _("The account {} has not enough surface for a deduction of {} m². There are only {} m² left").format( + acc.identifier, + format_german_float(form_surface), + format_german_float(rest_surface), + ), + ) + return is_valid_surface and super_result + + def save(self): + with transaction.atomic(): + # Create log entry + user_action_edit = UserActionLogEntry.objects.create( + user=self.user, + action=UserAction.EDITED + ) + user_action_create = UserActionLogEntry.objects.create( + user=self.user, + action=UserAction.CREATED + ) + self.instance.log.add(user_action_edit) + self.instance.modified = user_action_edit + self.instance.save() + + # Create deductions depending on Intervention or EcoAccount as the initial instance + if self.is_intervention_initially: + deduction = EcoAccountDeduction.objects.create( + intervention=self.instance, + account=self.cleaned_data["account"], + surface=self.cleaned_data["surface"], + created=user_action_create, + ) + else: + deduction = EcoAccountDeduction.objects.create( + intervention=self.cleaned_data["intervention"], + account=self.instance, + surface=self.cleaned_data["surface"], + created=user_action_create, + ) + return deduction \ No newline at end of file diff --git a/intervention/inputs.py b/intervention/inputs.py new file mode 100644 index 00000000..5781709a --- /dev/null +++ b/intervention/inputs.py @@ -0,0 +1,15 @@ +from django import forms + + +class DummyFilterInput(forms.HiddenInput): + """ A dummy input widget + + Does not render anything. Can be used to keep filter logic using django_filter without having a pre defined + filter widget being rendered to the template. + + """ + template_name = "konova/custom_widgets/dummy-filter-input.html" + + +class TextToClipboardInput(forms.TextInput): + template_name = "konova/custom_widgets/text-to-clipboard-input.html" \ No newline at end of file diff --git a/intervention/views.py b/intervention/views.py index fb40c98e..97ed5cf2 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -1,11 +1,11 @@ -from django.contrib import messages from django.contrib.auth.decorators import login_required from django.utils.translation import gettext_lazy as _ from django.http import HttpRequest from django.shortcuts import render, get_object_or_404 -from intervention.forms import NewInterventionForm, EditInterventionForm, ShareInterventionForm, NewRevocationForm, \ - RunCheckForm, NewDeductionForm +from intervention.forms.forms import NewInterventionForm, EditInterventionForm +from intervention.forms.modalForms import ShareInterventionModalForm, NewRevocationModalForm, \ + RunCheckModalForm, NewDeductionModalForm from intervention.models import Intervention, Revocation, InterventionDocument, RevocationDocument from intervention.tables import InterventionTable from konova.contexts import BaseContext @@ -342,7 +342,7 @@ def create_share_view(request: HttpRequest, id: str): """ intervention = get_object_or_404(Intervention, id=id) - form = ShareInterventionForm(request.POST or None, instance=intervention, request=request) + form = ShareInterventionModalForm(request.POST or None, instance=intervention, request=request) return form.process_request( request, msg_success=_("Share settings updated") @@ -361,7 +361,7 @@ def run_check_view(request: HttpRequest, id: str): """ intervention = get_object_or_404(Intervention, id=id) - form = RunCheckForm(request.POST or None, instance=intervention, user=request.user) + form = RunCheckModalForm(request.POST or None, instance=intervention, user=request.user) return form.process_request( request, msg_success=_("Check performed"), @@ -381,7 +381,7 @@ def new_revocation_view(request: HttpRequest, id: str): """ intervention = get_object_or_404(Intervention, id=id) - form = NewRevocationForm(request.POST or None, request.FILES or None, instance=intervention, user=request.user) + form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, user=request.user) return form.process_request( request, msg_success=_("Revocation added") @@ -425,7 +425,7 @@ def new_deduction_view(request: HttpRequest, id: str): """ intervention = get_object_or_404(Intervention, id=id) - form = NewDeductionForm(request.POST or None, instance=intervention, user=request.user) + form = NewDeductionModalForm(request.POST or None, instance=intervention, user=request.user) return form.process_request( request, msg_success=_("Deduction added")