* refactors large forms.py into forms/modalForms.py and forms/forms.py
* refactors custom input fields into intervention/inputs.py
This commit is contained in:
mpeltriaux 2021-09-27 11:45:13 +02:00
parent 58eefbdbe6
commit fdb6f2d105
7 changed files with 772 additions and 754 deletions

View File

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

View File

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

View File

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

362
intervention/forms/forms.py Normal file
View File

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

View File

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

15
intervention/inputs.py Normal file
View File

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

View File

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