konova/konova/forms.py
mipel 24a6b7fd37 Konova code improvements
* improves rendering of codes as string (short names will be used if existing)
* reduces selectable konova codes to leafs (as discussed with experts)
* automatically resizes the width of modal form fields to w-100
* adds/updates translations
2021-08-24 14:50:51 +02:00

428 lines
14 KiB
Python

"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.20
"""
from abc import abstractmethod
from bootstrap_modal_forms.forms import BSModalForm
from bootstrap_modal_forms.utils import is_ajax
from django import forms
from django.contrib import messages
from django.contrib.auth.models import User
from django.contrib.gis.forms import GeometryField, OSMWidget
from django.contrib.gis.geos import Polygon
from django.db import transaction
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import render
from django.utils import timezone
from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount
from ema.models import Ema
from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.models import Document, BaseObject
from konova.utils.message_templates import FORM_INVALID
from user.models import UserActionLogEntry, UserAction
class BaseForm(forms.Form):
"""
Basic form for that holds attributes needed in all other forms
"""
template = None
action_url = None
form_title = None
cancel_redirect = None
form_caption = None
instance = None # The data holding model object
form_attrs = {} # Holds additional attributes, that can be used in the template
has_required_fields = False # Automatically set. Triggers hint rendering in templates
def __init__(self, *args, **kwargs):
self.instance = kwargs.pop("instance", None)
self.user = kwargs.pop("user", None)
super().__init__(*args, **kwargs)
# Check for required fields
for _field_name, _field_val in self.fields.items():
if _field_val.required:
self.has_required_fields = True
break
@abstractmethod
def save(self):
# To be implemented in subclasses!
pass
def disable_form_field(self, field: str):
"""
Disables a form field for user editing
"""
self.fields[field].widget.attrs["readonly"] = True
self.fields[field].disabled = True
self.fields[field].widget.attrs["title"] = _("Not editable")
def initialize_form_field(self, field: str, val):
"""
Initializes a form field with a value
"""
self.fields[field].initial = val
def add_placeholder_for_field(self, field: str, val):
"""
Adds a placeholder to a field after initialization
Args:
field (str): Field name
val (str): Placeholder
Returns:
"""
self.fields[field].widget.attrs["placeholder"] = val
def load_initial_data(self, form_data: dict, disabled_fields: list):
""" Initializes form data from instance
Inserts instance data into form and disables form fields
Returns:
"""
if self.instance is None:
return
for k, v in form_data.items():
self.initialize_form_field(k, v)
for field in disabled_fields:
self.disable_form_field(field)
class RemoveForm(BaseForm):
check = forms.BooleanField(
label=_("Confirm"),
label_suffix=_(""),
required=True,
)
def __init__(self, *args, **kwargs):
self.object_to_remove = kwargs.pop("object_to_remove", None)
self.remove_post_url = kwargs.pop("remove_post_url", "")
self.cancel_url = kwargs.pop("cancel_url", "")
super().__init__(*args, **kwargs)
self.form_title = _("Remove")
if self.object_to_remove is not None:
self.form_caption = _("You are about to remove {} {}").format(self.object_to_remove.__class__.__name__, self.object_to_remove)
self.action_url = self.remove_post_url
self.cancel_redirect = self.cancel_url
def is_checked(self) -> bool:
return self.cleaned_data.get("check", False)
def save(self, user: User):
""" Perform generic removing by running the form typical 'save()' method
Args:
user (User): The performing user
Returns:
"""
if self.object_to_remove is not None and self.is_checked():
with transaction.atomic():
self.object_to_remove.is_active = False
action = UserActionLogEntry.objects.create(
user=user,
timestamp=timezone.now(),
action=UserAction.DELETED
)
self.object_to_remove.deleted = action
self.object_to_remove.save()
return self.object_to_remove
class BaseModalForm(BaseForm, BSModalForm):
""" A specialzed form class for modal form handling
"""
is_modal_form = True
render_submit = True
template = "modal/modal_form.html"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Automatically add bootstrap w-100 class for maximum width of form fields in modals
for key, val in self.fields.items():
val.widget.attrs.update(
{
"class": "w-100"
}
)
def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
""" Generic processing of request
Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used
Args:
request (HttpRequest): The incoming request
msg_success (str): The message in case of successful removing
msg_error (str): The message in case of an error
Returns:
"""
redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home")
template = self.template
if request.method == "POST":
if self.is_valid():
if not is_ajax(request.META):
# Modal forms send one POST for checking on data validity. This can be used to return possible errors
# on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
# saving/commiting of the data to the database. is_ajax() performs this check. The first request is
# an ajax call, the second is a regular form POST.
self.save()
messages.success(
request,
msg_success
)
return HttpResponseRedirect(redirect_url)
else:
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
elif request.method == "GET":
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
else:
raise NotImplementedError
class SimpleGeomForm(BaseForm):
""" A geometry form for rendering geometry read-only using a widget
"""
geom = GeometryField(
required=False,
disabled=True,
widget=OSMWidget(
attrs={
"map_width": 600,
"map_height": 400,
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Initialize geometry
try:
geom = self.instance.geometry.geom
if geom is None:
raise AttributeError
except AttributeError:
# catches if no geometry has been added, yet. Replace with empty placeholder polygon.
geom = Polygon.from_bbox([0, 0, 0, 0])
# Zoom out to a very high level, so the user can see directly that there is no geometry for this entry
self.fields["geom"].widget.attrs["default_zoom"] = 1
self.initialize_form_field("geom", geom)
self.area = geom.area
class RemoveModalForm(BaseModalForm):
""" Generic removing modal form
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
confirm = forms.BooleanField(
label=_("Confirm"),
label_suffix=_(""),
widget=forms.CheckboxInput(),
required=True,
)
def __init__(self, *args, **kwargs):
self.template = "modal/modal_form.html"
super().__init__(*args, **kwargs)
self.form_title = _("Remove")
self.form_caption = _("Are you sure?")
# Disable automatic w-100 setting for this type of modal form. Looks kinda strange
self.fields["confirm"].widget.attrs["class"] = ""
def save(self):
if isinstance(self.instance, BaseObject):
with transaction.atomic():
action = UserActionLogEntry.objects.create(
user=self.user,
timestamp=timezone.now(),
action=UserAction.DELETED,
)
self.instance.deleted = action
self.instance.log.add(action)
self.instance.save()
else:
# If the class does not provide restorable delete functionality, we must delete the entry finally
self.instance.delete()
class NewDocumentForm(BaseModalForm):
""" Modal form for new documents
"""
title = forms.CharField(
label=_("Title"),
label_suffix=_(""),
max_length=500,
)
creation_date = forms.DateField(
label=_("Created on"),
label_suffix=_(""),
help_text=_("When has this file been created? Important for photos."),
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
},
format="%d.%m.%Y"
)
)
file = forms.FileField(
label=_("File"),
label_suffix=_(""),
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,
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Add new document")
self.form_caption = _("")
self.template = "modal/modal_form.html"
self.form_attrs = {
"enctype": "multipart/form-data", # important for file upload
}
def save(self):
with transaction.atomic():
action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.CREATED,
)
doc = Document.objects.create(
created=action,
title=self.cleaned_data["title"],
comment=self.cleaned_data["comment"],
file=self.cleaned_data["file"],
date_of_creation=self.cleaned_data["creation_date"],
)
self.instance.documents.add(doc)
edited_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.EDITED,
comment=_("Added document"),
)
self.instance.log.add(edited_action)
self.instance.modified = edited_action
self.instance.save()
return doc
class RecordForm(BaseModalForm):
""" Modal form for recording data
"""
confirm = forms.BooleanField(
label=_("Confirm record"),
label_suffix="",
widget=forms.CheckboxInput(),
required=True,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Record data")
self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name)
if self.instance.recorded:
# unrecord!
self.fields["confirm"].label = _("Confirm unrecord")
self.form_title = _("Unrecord data")
self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name)
implemented_cls_logic = {
Intervention,
EcoAccount,
Ema,
}
instance_name = self.instance.__class__
if instance_name not in implemented_cls_logic:
raise NotImplementedError
def is_valid(self):
""" Checks for instance's validity and data quality
Returns:
"""
super_val = super().is_valid()
msgs = self.instance.quality_check() or []
for msg in msgs:
self.add_error(
"confirm",
msg
)
return super_val and (len(msgs) == 0)
def save(self):
with transaction.atomic():
if self.cleaned_data["confirm"]:
if self.instance.recorded:
# unrecord!
unrecord_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.UNRECORDED
)
# Do not delete the old .recorded attribute, since it shall stay in the .log list!
self.instance.recorded = None
self.instance.log.add(unrecord_action)
else:
record_action = UserActionLogEntry.objects.create(
user=self.user,
action=UserAction.RECORDED
)
self.instance.recorded = record_action
self.instance.log.add(record_action)
self.instance.save()
return self.instance