Migrations + Cleanup
* adds needed migrations * refactors forms.py (700+ lines) in main konova app * splits into forms/ and forms/modals and single class/topic-files for better maintainability and overview * fixes bug in main konova app migration which could occur if a certain compensation migration did not run before
This commit is contained in:
parent
8bce8b8e75
commit
a6f7e605e6
@ -20,7 +20,7 @@ from compensation.models import CompensationDocument, EcoAccountDocument
|
||||
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \
|
||||
CompensationStateTreeRadioSelect
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
|
||||
from konova.forms.modals import BaseModalForm, NewDocumentModalForm, RemoveModalForm
|
||||
from konova.models import DeadlineType
|
||||
from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, \
|
||||
ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
|
||||
|
24
compensation/migrations/0008_auto_20220815_0803.py
Normal file
24
compensation/migrations/0008_auto_20220815_0803.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.3 on 2022-08-15 06:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0014_resubmission'),
|
||||
('compensation', '0007_auto_20220531_1245'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='compensation',
|
||||
name='resubmission',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='_compensation_resubmission_+', to='konova.Resubmission'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ecoaccount',
|
||||
name='resubmission',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='_ecoaccount_resubmission_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
32
compensation/migrations/0009_auto_20220815_0803.py
Normal file
32
compensation/migrations/0009_auto_20220815_0803.py
Normal file
@ -0,0 +1,32 @@
|
||||
# Generated by Django 3.1.3 on 2022-08-15 06:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0014_resubmission'),
|
||||
('compensation', '0008_auto_20220815_0803'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='compensation',
|
||||
name='resubmission',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='ecoaccount',
|
||||
name='resubmission',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='compensation',
|
||||
name='resubmissions',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='_compensation_resubmissions_+', to='konova.Resubmission'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ecoaccount',
|
||||
name='resubmissions',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='_ecoaccount_resubmissions_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
24
compensation/migrations/0010_auto_20220815_1030.py
Normal file
24
compensation/migrations/0010_auto_20220815_1030.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Generated by Django 3.1.3 on 2022-08-15 08:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0014_resubmission'),
|
||||
('compensation', '0009_auto_20220815_0803'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='compensation',
|
||||
name='resubmissions',
|
||||
field=models.ManyToManyField(blank=True, related_name='_compensation_resubmissions_+', to='konova.Resubmission'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='ecoaccount',
|
||||
name='resubmissions',
|
||||
field=models.ManyToManyField(blank=True, related_name='_ecoaccount_resubmissions_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
@ -14,8 +14,9 @@ from compensation.tables import CompensationTable
|
||||
from intervention.models import Intervention
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import *
|
||||
from konova.forms import RemoveModalForm, SimpleGeomForm, RemoveDeadlineModalForm, EditDocumentModalForm, \
|
||||
from konova.forms.modals import RemoveModalForm,RemoveDeadlineModalForm, EditDocumentModalForm, \
|
||||
ResubmissionModalForm
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.models import Deadline
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.documents import get_document, remove_document
|
||||
|
@ -25,14 +25,15 @@ from intervention.forms.modalForms import NewDeductionModalForm, ShareModalForm,
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \
|
||||
shared_access_required
|
||||
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentModalForm, RecordModalForm, \
|
||||
from konova.forms.modals import RemoveModalForm, RecordModalForm, \
|
||||
RemoveDeadlineModalForm, EditDocumentModalForm, ResubmissionModalForm
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.models import Deadline
|
||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.documents import get_document, remove_document
|
||||
from konova.utils.generators import generate_qr_code
|
||||
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
|
||||
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, \
|
||||
CANCEL_ACC_RECORDED_OR_DEDUCTED, DEDUCTION_REMOVED, DEDUCTION_ADDED, DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, \
|
||||
COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, \
|
||||
DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, \
|
||||
|
@ -15,7 +15,6 @@ from compensation.forms.modalForms import NewPaymentForm, RemovePaymentModalForm
|
||||
from compensation.models import Payment
|
||||
from intervention.models import Intervention
|
||||
from konova.decorators import default_group_required, shared_access_required
|
||||
from konova.forms import RemoveModalForm
|
||||
from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED
|
||||
|
||||
|
||||
|
@ -5,8 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 06.10.21
|
||||
|
||||
"""
|
||||
from dal import autocomplete
|
||||
from django import forms
|
||||
from user.models import User
|
||||
from django.db import transaction
|
||||
from django.urls import reverse, reverse_lazy
|
||||
@ -16,7 +14,8 @@ from compensation.forms.forms import AbstractCompensationForm, CompensationRespo
|
||||
PikCompensationFormMixin
|
||||
from ema.models import Ema, EmaDocument
|
||||
from intervention.models import Responsibility, Handler
|
||||
from konova.forms import SimpleGeomForm, NewDocumentModalForm
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.forms.modals import NewDocumentModalForm
|
||||
from user.models import UserActionLogEntry
|
||||
|
||||
|
||||
|
19
ema/migrations/0005_ema_resubmission.py
Normal file
19
ema/migrations/0005_ema_resubmission.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.1.3 on 2022-08-15 06:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0014_resubmission'),
|
||||
('ema', '0004_ema_is_pik'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='ema',
|
||||
name='resubmission',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='_ema_resubmission_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
23
ema/migrations/0006_auto_20220815_0803.py
Normal file
23
ema/migrations/0006_auto_20220815_0803.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.3 on 2022-08-15 06:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0014_resubmission'),
|
||||
('ema', '0005_ema_resubmission'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='ema',
|
||||
name='resubmission',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='ema',
|
||||
name='resubmissions',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='_ema_resubmissions_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
19
ema/migrations/0007_auto_20220815_1030.py
Normal file
19
ema/migrations/0007_auto_20220815_1030.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.1.3 on 2022-08-15 08:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0014_resubmission'),
|
||||
('ema', '0006_auto_20220815_0803'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='ema',
|
||||
name='resubmissions',
|
||||
field=models.ManyToManyField(blank=True, related_name='_ema_resubmissions_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
@ -16,8 +16,9 @@ from intervention.forms.modalForms import ShareModalForm
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import conservation_office_group_required, shared_access_required
|
||||
from ema.models import Ema, EmaDocument
|
||||
from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm, RemoveDeadlineModalForm, \
|
||||
from konova.forms.modals import RemoveModalForm, RecordModalForm, RemoveDeadlineModalForm, \
|
||||
EditDocumentModalForm, ResubmissionModalForm
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.models import Deadline
|
||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
|
@ -8,6 +8,7 @@ Created on: 02.12.20
|
||||
from dal import autocomplete
|
||||
from django import forms
|
||||
|
||||
from konova.forms.base_form import BaseForm
|
||||
from konova.utils.message_templates import EDITED_GENERAL_DATA
|
||||
from user.models import User
|
||||
from django.db import transaction
|
||||
@ -19,7 +20,7 @@ from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \
|
||||
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
|
||||
from intervention.inputs import GenerateInput
|
||||
from intervention.models import Intervention, Legal, Responsibility, Handler
|
||||
from konova.forms import BaseForm, SimpleGeomForm
|
||||
from konova.forms.geometry_form import SimpleGeomForm
|
||||
from user.models import UserActionLogEntry
|
||||
|
||||
|
||||
|
@ -19,7 +19,8 @@ from django.utils.translation import gettext_lazy as _
|
||||
from compensation.models import EcoAccount, EcoAccountDeduction
|
||||
from intervention.inputs import TextToClipboardInput
|
||||
from intervention.models import Intervention, InterventionDocument, RevocationDocument
|
||||
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
|
||||
from konova.forms.modals import BaseModalForm
|
||||
from konova.forms.modals import NewDocumentModalForm, RemoveModalForm
|
||||
from konova.utils.general import format_german_float
|
||||
from konova.utils.user_checks import is_default_group_only
|
||||
|
||||
|
19
intervention/migrations/0005_intervention_resubmission.py
Normal file
19
intervention/migrations/0005_intervention_resubmission.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.1.3 on 2022-08-15 06:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0014_resubmission'),
|
||||
('intervention', '0004_auto_20220303_0956'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='intervention',
|
||||
name='resubmission',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='_intervention_resubmission_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
23
intervention/migrations/0006_auto_20220815_0803.py
Normal file
23
intervention/migrations/0006_auto_20220815_0803.py
Normal file
@ -0,0 +1,23 @@
|
||||
# Generated by Django 3.1.3 on 2022-08-15 06:03
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0014_resubmission'),
|
||||
('intervention', '0005_intervention_resubmission'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveField(
|
||||
model_name='intervention',
|
||||
name='resubmission',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='intervention',
|
||||
name='resubmissions',
|
||||
field=models.ManyToManyField(blank=True, null=True, related_name='_intervention_resubmissions_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
19
intervention/migrations/0007_auto_20220815_1030.py
Normal file
19
intervention/migrations/0007_auto_20220815_1030.py
Normal file
@ -0,0 +1,19 @@
|
||||
# Generated by Django 3.1.3 on 2022-08-15 08:30
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0014_resubmission'),
|
||||
('intervention', '0006_auto_20220815_0803'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='intervention',
|
||||
name='resubmissions',
|
||||
field=models.ManyToManyField(blank=True, related_name='_intervention_resubmissions_+', to='konova.Resubmission'),
|
||||
),
|
||||
]
|
@ -12,7 +12,8 @@ from intervention.models import Intervention, Revocation, InterventionDocument,
|
||||
from intervention.tables import InterventionTable
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import *
|
||||
from konova.forms import SimpleGeomForm, RemoveModalForm, RecordModalForm, EditDocumentModalForm, ResubmissionModalForm
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.forms.modals import RemoveModalForm, RecordModalForm, EditDocumentModalForm, ResubmissionModalForm
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.documents import remove_document, get_document
|
||||
from konova.utils.generators import generate_qr_code
|
||||
|
760
konova/forms.py
760
konova/forms.py
@ -1,760 +0,0 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 16.11.20
|
||||
|
||||
"""
|
||||
import json
|
||||
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.gis import gdal
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models.fields.files import FieldFile
|
||||
from django.utils.timezone import now
|
||||
|
||||
from compensation.models import EcoAccount
|
||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
|
||||
from user.models import User
|
||||
from django.contrib.gis.forms import MultiPolygonField
|
||||
from django.contrib.gis.geos import MultiPolygon, Polygon
|
||||
from django.db import transaction
|
||||
from django.http import HttpRequest, HttpResponseRedirect
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.contexts import BaseContext
|
||||
from konova.models import BaseObject, Geometry, RecordableObjectMixin, AbstractDocument, Resubmission
|
||||
from konova.settings import DEFAULT_SRID
|
||||
from konova.tasks import celery_update_parcels
|
||||
from konova.utils.message_templates import FORM_INVALID, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE, DOCUMENT_EDITED
|
||||
from user.models import UserActionLogEntry
|
||||
|
||||
|
||||
class BaseForm(forms.Form):
|
||||
"""
|
||||
Basic form for that holds attributes needed in all other forms
|
||||
"""
|
||||
template = None
|
||||
action_url = None
|
||||
action_btn_label = _("Save")
|
||||
form_title = None
|
||||
cancel_redirect = None
|
||||
form_caption = None
|
||||
instance = None # The data holding model object
|
||||
request = None
|
||||
form_attrs = {} # Holds additional attributes, that can be used in the template
|
||||
has_required_fields = False # Automatically set. Triggers hint rendering in templates
|
||||
show_cancel_btn = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.instance = kwargs.pop("instance", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.request is not None:
|
||||
self.user = self.request.user
|
||||
# Check for required fields
|
||||
for _field_name, _field_val in self.fields.items():
|
||||
if _field_val.required:
|
||||
self.has_required_fields = True
|
||||
break
|
||||
|
||||
self.check_for_recorded_instance()
|
||||
|
||||
@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 without the need to redefine the form widget
|
||||
|
||||
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 = None):
|
||||
""" 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)
|
||||
if disabled_fields:
|
||||
for field in disabled_fields:
|
||||
self.disable_form_field(field)
|
||||
|
||||
def add_widget_html_class(self, field: str, cls: str):
|
||||
""" Adds a HTML class string to the widget of a field
|
||||
|
||||
Args:
|
||||
field (str): The field's name
|
||||
cls (str): The new class string
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
set_class = self.fields[field].widget.attrs.get("class", "")
|
||||
if cls in set_class:
|
||||
return
|
||||
else:
|
||||
set_class += " " + cls
|
||||
self.fields[field].widget.attrs["class"] = set_class
|
||||
|
||||
def remove_widget_html_class(self, field: str, cls: str):
|
||||
""" Removes a HTML class string from the widget of a field
|
||||
|
||||
Args:
|
||||
field (str): The field's name
|
||||
cls (str): The new class string
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
set_class = self.fields[field].widget.attrs.get("class", "")
|
||||
set_class = set_class.replace(cls, "")
|
||||
self.fields[field].widget.attrs["class"] = set_class
|
||||
|
||||
def check_for_recorded_instance(self):
|
||||
""" Checks if the instance is recorded and runs some special logic if yes
|
||||
|
||||
If the instance is recorded, the form shall not display any possibility to
|
||||
edit any data. Instead, the users should get some information about why they can not edit anything.
|
||||
|
||||
There are situations where the form should be rendered regularly,
|
||||
e.g deduction forms for (recorded) eco accounts.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \
|
||||
RemoveEcoAccountDeductionModalForm
|
||||
is_none = self.instance is None
|
||||
is_other_data_type = not isinstance(self.instance, BaseObject)
|
||||
is_deduction_form_from_account = isinstance(
|
||||
self,
|
||||
(
|
||||
NewDeductionModalForm,
|
||||
ResubmissionModalForm,
|
||||
EditEcoAccountDeductionModalForm,
|
||||
RemoveEcoAccountDeductionModalForm,
|
||||
)
|
||||
) and isinstance(self.instance, EcoAccount)
|
||||
|
||||
if is_none or is_other_data_type or is_deduction_form_from_account:
|
||||
# Do nothing
|
||||
return
|
||||
|
||||
if self.instance.is_recorded:
|
||||
self.template = "form/recorded_no_edit.html"
|
||||
|
||||
|
||||
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.get_deleted_action(user)
|
||||
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)
|
||||
self.action_btn_label = _("Continue")
|
||||
|
||||
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
|
||||
|
||||
"""
|
||||
read_only = True
|
||||
geom = MultiPolygonField(
|
||||
srid=DEFAULT_SRID_RLP,
|
||||
label=_("Geometry"),
|
||||
help_text=_(""),
|
||||
label_suffix="",
|
||||
required=False,
|
||||
disabled=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.read_only = kwargs.pop("read_only", True)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Initialize geometry
|
||||
try:
|
||||
geom = self.instance.geometry.geom
|
||||
self.empty = geom.empty
|
||||
|
||||
if self.empty:
|
||||
raise AttributeError
|
||||
|
||||
geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
|
||||
geom = json.dumps(geojson)
|
||||
except AttributeError:
|
||||
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
|
||||
geom = ""
|
||||
self.empty = True
|
||||
|
||||
self.initialize_form_field("geom", geom)
|
||||
|
||||
def is_valid(self):
|
||||
super().is_valid()
|
||||
is_valid = True
|
||||
|
||||
# Get geojson from form
|
||||
geom = self.data["geom"]
|
||||
if geom is None or len(geom) == 0:
|
||||
# empty geometry is a valid geometry
|
||||
return is_valid
|
||||
geom = json.loads(geom)
|
||||
|
||||
# Write submitted data back into form field to make sure invalid geometry
|
||||
# will be rendered again on failed submit
|
||||
self.initialize_form_field("geom", self.data["geom"])
|
||||
|
||||
# Read geojson into gdal geometry
|
||||
# HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for
|
||||
# this case)
|
||||
features = []
|
||||
features_json = geom.get("features", [])
|
||||
for feature in features_json:
|
||||
g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP)
|
||||
if g.geom_type not in ["Polygon", "MultiPolygon"]:
|
||||
self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
|
||||
is_valid = False
|
||||
return is_valid
|
||||
|
||||
polygon = Polygon.from_ewkt(g.ewkt)
|
||||
is_valid = polygon.valid
|
||||
if not is_valid:
|
||||
self.add_error("geom", polygon.valid_reason)
|
||||
return is_valid
|
||||
|
||||
features.append(polygon)
|
||||
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
|
||||
for feature in features:
|
||||
form_geom = form_geom.union(feature)
|
||||
|
||||
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
|
||||
if form_geom.geom_type != "MultiPolygon":
|
||||
form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP)
|
||||
|
||||
# Write unioned Multipolygon into cleaned data
|
||||
if self.cleaned_data is None:
|
||||
self.cleaned_data = {}
|
||||
self.cleaned_data["geom"] = form_geom.ewkt
|
||||
|
||||
return is_valid
|
||||
|
||||
def save(self, action: UserActionLogEntry):
|
||||
""" Saves the form's geometry
|
||||
|
||||
Creates a new geometry entry if none is set, yet
|
||||
|
||||
Args:
|
||||
action ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
try:
|
||||
if self.instance is None or self.instance.geometry is None:
|
||||
raise LookupError
|
||||
geometry = self.instance.geometry
|
||||
geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP))
|
||||
geometry.modified = action
|
||||
|
||||
geometry.save()
|
||||
except LookupError:
|
||||
# No geometry or linked instance holding a geometry exist --> create a new one!
|
||||
geometry = Geometry.objects.create(
|
||||
geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)),
|
||||
created=action,
|
||||
)
|
||||
# Start the parcel update procedure in a background process
|
||||
celery_update_parcels.delay(geometry.id)
|
||||
return geometry
|
||||
|
||||
|
||||
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):
|
||||
self.instance.mark_as_deleted(self.user)
|
||||
else:
|
||||
# If the class does not provide restorable delete functionality, we must delete the entry finally
|
||||
self.instance.delete()
|
||||
|
||||
|
||||
class RemoveDeadlineModalForm(RemoveModalForm):
|
||||
""" Removing modal form for deadlines
|
||||
|
||||
Can be used for anything, where removing shall be confirmed by the user a second time.
|
||||
|
||||
"""
|
||||
deadline = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
deadline = kwargs.pop("deadline", None)
|
||||
self.deadline = deadline
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self):
|
||||
self.instance.remove_deadline(self)
|
||||
|
||||
|
||||
class NewDocumentModalForm(BaseModalForm):
|
||||
""" Modal form for new documents
|
||||
|
||||
"""
|
||||
title = forms.CharField(
|
||||
label=_("Title"),
|
||||
label_suffix=_(""),
|
||||
max_length=500,
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
}
|
||||
)
|
||||
)
|
||||
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",
|
||||
"class": "form-control",
|
||||
},
|
||||
format="%d.%m.%Y"
|
||||
)
|
||||
)
|
||||
file = forms.FileField(
|
||||
label=_("File"),
|
||||
label_suffix=_(""),
|
||||
help_text=_("Allowed formats: pdf, jpg, png. Max size 15 MB."),
|
||||
widget=forms.FileInput(
|
||||
attrs={
|
||||
"class": "form-control-file",
|
||||
}
|
||||
),
|
||||
)
|
||||
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,
|
||||
"class": "form-control",
|
||||
}
|
||||
)
|
||||
)
|
||||
document_model = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Add new document")
|
||||
self.form_caption = _("")
|
||||
self.form_attrs = {
|
||||
"enctype": "multipart/form-data", # important for file upload
|
||||
}
|
||||
if not self.document_model:
|
||||
raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__))
|
||||
|
||||
def is_valid(self):
|
||||
super_valid = super().is_valid()
|
||||
|
||||
_file = self.cleaned_data.get("file", None)
|
||||
|
||||
if _file is None or isinstance(_file, FieldFile):
|
||||
# FieldFile declares that no new file has been uploaded and we do not need to check on the file again
|
||||
return super_valid
|
||||
|
||||
mime_type_valid = self.document_model.is_mime_type_valid(_file)
|
||||
if not mime_type_valid:
|
||||
self.add_error(
|
||||
"file",
|
||||
FILE_TYPE_UNSUPPORTED
|
||||
)
|
||||
|
||||
file_size_valid = self.document_model.is_file_size_valid(_file)
|
||||
if not file_size_valid:
|
||||
self.add_error(
|
||||
"file",
|
||||
FILE_SIZE_TOO_LARGE
|
||||
)
|
||||
|
||||
file_valid = mime_type_valid and file_size_valid
|
||||
return super_valid and file_valid
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
action = UserActionLogEntry.get_created_action(self.user)
|
||||
edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document"))
|
||||
|
||||
doc = self.document_model.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"],
|
||||
instance=self.instance,
|
||||
)
|
||||
|
||||
self.instance.log.add(edited_action)
|
||||
self.instance.modified = edited_action
|
||||
self.instance.save()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
class EditDocumentModalForm(NewDocumentModalForm):
|
||||
document = None
|
||||
document_model = AbstractDocument
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.document = kwargs.pop("document", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Edit document")
|
||||
form_data = {
|
||||
"title": self.document.title,
|
||||
"comment": self.document.comment,
|
||||
"creation_date": str(self.document.date_of_creation),
|
||||
"file": self.document.file,
|
||||
}
|
||||
self.load_initial_data(form_data)
|
||||
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
document = self.document
|
||||
file = self.cleaned_data.get("file", None)
|
||||
|
||||
document.title = self.cleaned_data.get("title", None)
|
||||
document.comment = self.cleaned_data.get("comment", None)
|
||||
document.date_of_creation = self.cleaned_data.get("creation_date", None)
|
||||
if not isinstance(file, FieldFile):
|
||||
document.replace_file(file)
|
||||
document.save()
|
||||
|
||||
self.instance.mark_as_edited(self.user, self.request, edit_comment=DOCUMENT_EDITED)
|
||||
|
||||
return document
|
||||
|
||||
|
||||
class RecordModalForm(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)
|
||||
# Disable automatic w-100 setting for this type of modal form. Looks kinda strange
|
||||
self.fields["confirm"].widget.attrs["class"] = ""
|
||||
|
||||
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)
|
||||
|
||||
if not isinstance(self.instance, RecordableObjectMixin):
|
||||
raise NotImplementedError
|
||||
|
||||
def is_valid(self):
|
||||
""" Checks for instance's validity and data quality
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
from intervention.models import Intervention
|
||||
super_val = super().is_valid()
|
||||
if self.instance.recorded:
|
||||
# If user wants to unrecord an already recorded dataset, we do not need to perform custom checks
|
||||
return super_val
|
||||
checker = self.instance.quality_check()
|
||||
for msg in checker.messages:
|
||||
self.add_error(
|
||||
"confirm",
|
||||
msg
|
||||
)
|
||||
valid = checker.valid
|
||||
# Special case: Intervention
|
||||
# Add direct checks for related compensations
|
||||
if isinstance(self.instance, Intervention):
|
||||
comps_valid = self._are_compensations_valid()
|
||||
valid = valid and comps_valid
|
||||
return super_val and valid
|
||||
|
||||
def _are_deductions_valid(self):
|
||||
""" Performs validity checks on deductions and their eco-account
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
deductions = self.instance.deductions.all()
|
||||
for deduction in deductions:
|
||||
checker = deduction.account.quality_check()
|
||||
for msg in checker.messages:
|
||||
self.add_error(
|
||||
"confirm",
|
||||
f"{deduction.account.identifier}: {msg}"
|
||||
)
|
||||
return checker.valid
|
||||
return True
|
||||
|
||||
def _are_compensations_valid(self):
|
||||
""" Runs a special case for intervention-compensations validity
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
comps = self.instance.compensations.filter(
|
||||
deleted=None,
|
||||
)
|
||||
comps_valid = True
|
||||
for comp in comps:
|
||||
checker = comp.quality_check()
|
||||
comps_valid = comps_valid and checker.valid
|
||||
for msg in checker.messages:
|
||||
self.add_error(
|
||||
"confirm",
|
||||
f"{comp.identifier}: {msg}"
|
||||
)
|
||||
|
||||
deductions_valid = self._are_deductions_valid()
|
||||
|
||||
return comps_valid and deductions_valid
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
if self.cleaned_data["confirm"]:
|
||||
if self.instance.recorded:
|
||||
self.instance.set_unrecorded(self.user)
|
||||
else:
|
||||
self.instance.set_recorded(self.user)
|
||||
return self.instance
|
||||
|
||||
def check_for_recorded_instance(self):
|
||||
""" Overwrite the check method for doing nothing on the RecordModalForm
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
class ResubmissionModalForm(BaseModalForm):
|
||||
date = forms.DateField(
|
||||
label_suffix=_(""),
|
||||
label=_("Date"),
|
||||
help_text=_("When do you want to be reminded?"),
|
||||
widget=forms.DateInput(
|
||||
attrs={
|
||||
"type": "date",
|
||||
"data-provide": "datepicker",
|
||||
"class": "form-control",
|
||||
},
|
||||
format="%d.%m.%Y"
|
||||
)
|
||||
)
|
||||
comment = forms.CharField(
|
||||
required=False,
|
||||
label=_("Comment"),
|
||||
label_suffix=_(""),
|
||||
help_text=_("Additional comment"),
|
||||
widget=forms.Textarea(
|
||||
attrs={
|
||||
"cols": 30,
|
||||
"rows": 5,
|
||||
"class": "form-control",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Resubmission")
|
||||
self.form_caption = _("Set your resubmission for this entry.")
|
||||
self.action_url = None
|
||||
|
||||
try:
|
||||
self.resubmission = self.instance.resubmissions.get(
|
||||
user=self.user
|
||||
)
|
||||
self.initialize_form_field("date", str(self.resubmission.resubmit_on))
|
||||
self.initialize_form_field("comment", self.resubmission.comment)
|
||||
except ObjectDoesNotExist:
|
||||
self.resubmission = Resubmission()
|
||||
|
||||
def is_valid(self):
|
||||
super_valid = super().is_valid()
|
||||
self_valid = True
|
||||
|
||||
date = self.cleaned_data.get("date")
|
||||
today = now().today().date()
|
||||
if date <= today:
|
||||
self.add_error(
|
||||
"date",
|
||||
_("The date should be in the future")
|
||||
)
|
||||
self_valid = False
|
||||
|
||||
return super_valid and self_valid
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
self.resubmission.user = self.user
|
||||
self.resubmission.resubmit_on = self.cleaned_data.get("date")
|
||||
self.resubmission.comment = self.cleaned_data.get("comment")
|
||||
self.resubmission.save()
|
||||
self.instance.resubmissions.add(self.resubmission)
|
||||
return self.resubmission
|
||||
|
11
konova/forms/__init__.py
Normal file
11
konova/forms/__init__.py
Normal file
@ -0,0 +1,11 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
|
||||
from .base_form import *
|
||||
from .geometry_form import *
|
||||
from .remove_form import *
|
157
konova/forms/base_form.py
Normal file
157
konova/forms/base_form.py
Normal file
@ -0,0 +1,157 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from abc import abstractmethod
|
||||
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from compensation.models import EcoAccount
|
||||
from konova.models import BaseObject
|
||||
|
||||
|
||||
class BaseForm(forms.Form):
|
||||
"""
|
||||
Basic form for that holds attributes needed in all other forms
|
||||
"""
|
||||
template = None
|
||||
action_url = None
|
||||
action_btn_label = _("Save")
|
||||
form_title = None
|
||||
cancel_redirect = None
|
||||
form_caption = None
|
||||
instance = None # The data holding model object
|
||||
request = None
|
||||
form_attrs = {} # Holds additional attributes, that can be used in the template
|
||||
has_required_fields = False # Automatically set. Triggers hint rendering in templates
|
||||
show_cancel_btn = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.instance = kwargs.pop("instance", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.request is not None:
|
||||
self.user = self.request.user
|
||||
# Check for required fields
|
||||
for _field_name, _field_val in self.fields.items():
|
||||
if _field_val.required:
|
||||
self.has_required_fields = True
|
||||
break
|
||||
|
||||
self.check_for_recorded_instance()
|
||||
|
||||
@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 without the need to redefine the form widget
|
||||
|
||||
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 = None):
|
||||
""" 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)
|
||||
if disabled_fields:
|
||||
for field in disabled_fields:
|
||||
self.disable_form_field(field)
|
||||
|
||||
def add_widget_html_class(self, field: str, cls: str):
|
||||
""" Adds a HTML class string to the widget of a field
|
||||
|
||||
Args:
|
||||
field (str): The field's name
|
||||
cls (str): The new class string
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
set_class = self.fields[field].widget.attrs.get("class", "")
|
||||
if cls in set_class:
|
||||
return
|
||||
else:
|
||||
set_class += " " + cls
|
||||
self.fields[field].widget.attrs["class"] = set_class
|
||||
|
||||
def remove_widget_html_class(self, field: str, cls: str):
|
||||
""" Removes a HTML class string from the widget of a field
|
||||
|
||||
Args:
|
||||
field (str): The field's name
|
||||
cls (str): The new class string
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
set_class = self.fields[field].widget.attrs.get("class", "")
|
||||
set_class = set_class.replace(cls, "")
|
||||
self.fields[field].widget.attrs["class"] = set_class
|
||||
|
||||
def check_for_recorded_instance(self):
|
||||
""" Checks if the instance is recorded and runs some special logic if yes
|
||||
|
||||
If the instance is recorded, the form shall not display any possibility to
|
||||
edit any data. Instead, the users should get some information about why they can not edit anything.
|
||||
|
||||
There are situations where the form should be rendered regularly,
|
||||
e.g deduction forms for (recorded) eco accounts.
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \
|
||||
RemoveEcoAccountDeductionModalForm
|
||||
from konova.forms.modals.resubmission_form import ResubmissionModalForm
|
||||
is_none = self.instance is None
|
||||
is_other_data_type = not isinstance(self.instance, BaseObject)
|
||||
is_deduction_form_from_account = isinstance(
|
||||
self,
|
||||
(
|
||||
NewDeductionModalForm,
|
||||
ResubmissionModalForm,
|
||||
EditEcoAccountDeductionModalForm,
|
||||
RemoveEcoAccountDeductionModalForm,
|
||||
)
|
||||
) and isinstance(self.instance, EcoAccount)
|
||||
|
||||
if is_none or is_other_data_type or is_deduction_form_from_account:
|
||||
# Do nothing
|
||||
return
|
||||
|
||||
if self.instance.is_recorded:
|
||||
self.template = "form/recorded_no_edit.html"
|
133
konova/forms/geometry_form.py
Normal file
133
konova/forms/geometry_form.py
Normal file
@ -0,0 +1,133 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
import json
|
||||
|
||||
from django.contrib.gis import gdal
|
||||
from django.contrib.gis.forms import MultiPolygonField
|
||||
from django.contrib.gis.geos import MultiPolygon, Polygon
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.forms.base_form import BaseForm
|
||||
from konova.models import Geometry
|
||||
from konova.tasks import celery_update_parcels
|
||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
|
||||
from user.models import UserActionLogEntry
|
||||
|
||||
|
||||
class SimpleGeomForm(BaseForm):
|
||||
""" A geometry form for rendering geometry read-only using a widget
|
||||
|
||||
"""
|
||||
read_only = True
|
||||
geom = MultiPolygonField(
|
||||
srid=DEFAULT_SRID_RLP,
|
||||
label=_("Geometry"),
|
||||
help_text=_(""),
|
||||
label_suffix="",
|
||||
required=False,
|
||||
disabled=False,
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.read_only = kwargs.pop("read_only", True)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
# Initialize geometry
|
||||
try:
|
||||
geom = self.instance.geometry.geom
|
||||
self.empty = geom.empty
|
||||
|
||||
if self.empty:
|
||||
raise AttributeError
|
||||
|
||||
geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
|
||||
geom = json.dumps(geojson)
|
||||
except AttributeError:
|
||||
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
|
||||
geom = ""
|
||||
self.empty = True
|
||||
|
||||
self.initialize_form_field("geom", geom)
|
||||
|
||||
def is_valid(self):
|
||||
super().is_valid()
|
||||
is_valid = True
|
||||
|
||||
# Get geojson from form
|
||||
geom = self.data["geom"]
|
||||
if geom is None or len(geom) == 0:
|
||||
# empty geometry is a valid geometry
|
||||
return is_valid
|
||||
geom = json.loads(geom)
|
||||
|
||||
# Write submitted data back into form field to make sure invalid geometry
|
||||
# will be rendered again on failed submit
|
||||
self.initialize_form_field("geom", self.data["geom"])
|
||||
|
||||
# Read geojson into gdal geometry
|
||||
# HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for
|
||||
# this case)
|
||||
features = []
|
||||
features_json = geom.get("features", [])
|
||||
for feature in features_json:
|
||||
g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP)
|
||||
if g.geom_type not in ["Polygon", "MultiPolygon"]:
|
||||
self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
|
||||
is_valid = False
|
||||
return is_valid
|
||||
|
||||
polygon = Polygon.from_ewkt(g.ewkt)
|
||||
is_valid = polygon.valid
|
||||
if not is_valid:
|
||||
self.add_error("geom", polygon.valid_reason)
|
||||
return is_valid
|
||||
|
||||
features.append(polygon)
|
||||
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
|
||||
for feature in features:
|
||||
form_geom = form_geom.union(feature)
|
||||
|
||||
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
|
||||
if form_geom.geom_type != "MultiPolygon":
|
||||
form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP)
|
||||
|
||||
# Write unioned Multipolygon into cleaned data
|
||||
if self.cleaned_data is None:
|
||||
self.cleaned_data = {}
|
||||
self.cleaned_data["geom"] = form_geom.ewkt
|
||||
|
||||
return is_valid
|
||||
|
||||
def save(self, action: UserActionLogEntry):
|
||||
""" Saves the form's geometry
|
||||
|
||||
Creates a new geometry entry if none is set, yet
|
||||
|
||||
Args:
|
||||
action ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
try:
|
||||
if self.instance is None or self.instance.geometry is None:
|
||||
raise LookupError
|
||||
geometry = self.instance.geometry
|
||||
geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP))
|
||||
geometry.modified = action
|
||||
|
||||
geometry.save()
|
||||
except LookupError:
|
||||
# No geometry or linked instance holding a geometry exist --> create a new one!
|
||||
geometry = Geometry.objects.create(
|
||||
geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)),
|
||||
created=action,
|
||||
)
|
||||
# Start the parcel update procedure in a background process
|
||||
celery_update_parcels.delay(geometry.id)
|
||||
return geometry
|
12
konova/forms/modals/__init__.py
Normal file
12
konova/forms/modals/__init__.py
Normal file
@ -0,0 +1,12 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from .base_form import *
|
||||
from .document_form import *
|
||||
from .record_form import *
|
||||
from .remove_form import *
|
||||
from .resubmission_form import *
|
73
konova/forms/modals/base_form.py
Normal file
73
konova/forms/modals/base_form.py
Normal file
@ -0,0 +1,73 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from bootstrap_modal_forms.forms import BSModalForm
|
||||
from bootstrap_modal_forms.utils import is_ajax
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponseRedirect, HttpRequest
|
||||
from django.shortcuts import render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms.base_form import BaseForm
|
||||
from konova.utils.message_templates import FORM_INVALID
|
||||
|
||||
|
||||
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)
|
||||
self.action_btn_label = _("Continue")
|
||||
|
||||
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
|
163
konova/forms/modals/document_form.py
Normal file
163
konova/forms/modals/document_form.py
Normal file
@ -0,0 +1,163 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from django import forms
|
||||
from django.db import transaction
|
||||
from django.db.models.fields.files import FieldFile
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.forms.modals.base_form import BaseModalForm
|
||||
from konova.models import AbstractDocument
|
||||
from konova.utils.message_templates import DOCUMENT_EDITED, FILE_SIZE_TOO_LARGE, FILE_TYPE_UNSUPPORTED
|
||||
from user.models import UserActionLogEntry
|
||||
|
||||
|
||||
class NewDocumentModalForm(BaseModalForm):
|
||||
""" Modal form for new documents
|
||||
|
||||
"""
|
||||
title = forms.CharField(
|
||||
label=_("Title"),
|
||||
label_suffix=_(""),
|
||||
max_length=500,
|
||||
widget=forms.TextInput(
|
||||
attrs={
|
||||
"class": "form-control",
|
||||
}
|
||||
)
|
||||
)
|
||||
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",
|
||||
"class": "form-control",
|
||||
},
|
||||
format="%d.%m.%Y"
|
||||
)
|
||||
)
|
||||
file = forms.FileField(
|
||||
label=_("File"),
|
||||
label_suffix=_(""),
|
||||
help_text=_("Allowed formats: pdf, jpg, png. Max size 15 MB."),
|
||||
widget=forms.FileInput(
|
||||
attrs={
|
||||
"class": "form-control-file",
|
||||
}
|
||||
),
|
||||
)
|
||||
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,
|
||||
"class": "form-control",
|
||||
}
|
||||
)
|
||||
)
|
||||
document_model = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Add new document")
|
||||
self.form_caption = _("")
|
||||
self.form_attrs = {
|
||||
"enctype": "multipart/form-data", # important for file upload
|
||||
}
|
||||
if not self.document_model:
|
||||
raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__))
|
||||
|
||||
def is_valid(self):
|
||||
super_valid = super().is_valid()
|
||||
|
||||
_file = self.cleaned_data.get("file", None)
|
||||
|
||||
if _file is None or isinstance(_file, FieldFile):
|
||||
# FieldFile declares that no new file has been uploaded and we do not need to check on the file again
|
||||
return super_valid
|
||||
|
||||
mime_type_valid = self.document_model.is_mime_type_valid(_file)
|
||||
if not mime_type_valid:
|
||||
self.add_error(
|
||||
"file",
|
||||
FILE_TYPE_UNSUPPORTED
|
||||
)
|
||||
|
||||
file_size_valid = self.document_model.is_file_size_valid(_file)
|
||||
if not file_size_valid:
|
||||
self.add_error(
|
||||
"file",
|
||||
FILE_SIZE_TOO_LARGE
|
||||
)
|
||||
|
||||
file_valid = mime_type_valid and file_size_valid
|
||||
return super_valid and file_valid
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
action = UserActionLogEntry.get_created_action(self.user)
|
||||
edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document"))
|
||||
|
||||
doc = self.document_model.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"],
|
||||
instance=self.instance,
|
||||
)
|
||||
|
||||
self.instance.log.add(edited_action)
|
||||
self.instance.modified = edited_action
|
||||
self.instance.save()
|
||||
|
||||
return doc
|
||||
|
||||
|
||||
class EditDocumentModalForm(NewDocumentModalForm):
|
||||
document = None
|
||||
document_model = AbstractDocument
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.document = kwargs.pop("document", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Edit document")
|
||||
form_data = {
|
||||
"title": self.document.title,
|
||||
"comment": self.document.comment,
|
||||
"creation_date": str(self.document.date_of_creation),
|
||||
"file": self.document.file,
|
||||
}
|
||||
self.load_initial_data(form_data)
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
document = self.document
|
||||
file = self.cleaned_data.get("file", None)
|
||||
|
||||
document.title = self.cleaned_data.get("title", None)
|
||||
document.comment = self.cleaned_data.get("comment", None)
|
||||
document.date_of_creation = self.cleaned_data.get("creation_date", None)
|
||||
if not isinstance(file, FieldFile):
|
||||
document.replace_file(file)
|
||||
document.save()
|
||||
|
||||
self.instance.mark_as_edited(self.user, self.request, edit_comment=DOCUMENT_EDITED)
|
||||
|
||||
return document
|
||||
|
123
konova/forms/modals/record_form.py
Normal file
123
konova/forms/modals/record_form.py
Normal file
@ -0,0 +1,123 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from django import forms
|
||||
from django.db import transaction
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.forms.modals.base_form import BaseModalForm
|
||||
from konova.models import RecordableObjectMixin
|
||||
|
||||
|
||||
class RecordModalForm(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)
|
||||
# Disable automatic w-100 setting for this type of modal form. Looks kinda strange
|
||||
self.fields["confirm"].widget.attrs["class"] = ""
|
||||
|
||||
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)
|
||||
|
||||
if not isinstance(self.instance, RecordableObjectMixin):
|
||||
raise NotImplementedError
|
||||
|
||||
def is_valid(self):
|
||||
""" Checks for instance's validity and data quality
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
from intervention.models import Intervention
|
||||
super_val = super().is_valid()
|
||||
if self.instance.recorded:
|
||||
# If user wants to unrecord an already recorded dataset, we do not need to perform custom checks
|
||||
return super_val
|
||||
checker = self.instance.quality_check()
|
||||
for msg in checker.messages:
|
||||
self.add_error(
|
||||
"confirm",
|
||||
msg
|
||||
)
|
||||
valid = checker.valid
|
||||
# Special case: Intervention
|
||||
# Add direct checks for related compensations
|
||||
if isinstance(self.instance, Intervention):
|
||||
comps_valid = self._are_compensations_valid()
|
||||
valid = valid and comps_valid
|
||||
return super_val and valid
|
||||
|
||||
def _are_deductions_valid(self):
|
||||
""" Performs validity checks on deductions and their eco-account
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
deductions = self.instance.deductions.all()
|
||||
for deduction in deductions:
|
||||
checker = deduction.account.quality_check()
|
||||
for msg in checker.messages:
|
||||
self.add_error(
|
||||
"confirm",
|
||||
f"{deduction.account.identifier}: {msg}"
|
||||
)
|
||||
return checker.valid
|
||||
return True
|
||||
|
||||
def _are_compensations_valid(self):
|
||||
""" Runs a special case for intervention-compensations validity
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
comps = self.instance.compensations.filter(
|
||||
deleted=None,
|
||||
)
|
||||
comps_valid = True
|
||||
for comp in comps:
|
||||
checker = comp.quality_check()
|
||||
comps_valid = comps_valid and checker.valid
|
||||
for msg in checker.messages:
|
||||
self.add_error(
|
||||
"confirm",
|
||||
f"{comp.identifier}: {msg}"
|
||||
)
|
||||
|
||||
deductions_valid = self._are_deductions_valid()
|
||||
|
||||
return comps_valid and deductions_valid
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
if self.cleaned_data["confirm"]:
|
||||
if self.instance.recorded:
|
||||
self.instance.set_unrecorded(self.user)
|
||||
else:
|
||||
self.instance.set_recorded(self.user)
|
||||
return self.instance
|
||||
|
||||
def check_for_recorded_instance(self):
|
||||
""" Overwrite the check method for doing nothing on the RecordModalForm
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
pass
|
58
konova/forms/modals/remove_form.py
Normal file
58
konova/forms/modals/remove_form.py
Normal file
@ -0,0 +1,58 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from django import forms
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.forms.modals.base_form import BaseModalForm
|
||||
from konova.models import BaseObject
|
||||
|
||||
|
||||
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):
|
||||
self.instance.mark_as_deleted(self.user)
|
||||
else:
|
||||
# If the class does not provide restorable delete functionality, we must delete the entry finally
|
||||
self.instance.delete()
|
||||
|
||||
|
||||
class RemoveDeadlineModalForm(RemoveModalForm):
|
||||
""" Removing modal form for deadlines
|
||||
|
||||
Can be used for anything, where removing shall be confirmed by the user a second time.
|
||||
|
||||
"""
|
||||
deadline = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
deadline = kwargs.pop("deadline", None)
|
||||
self.deadline = deadline
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self):
|
||||
self.instance.remove_deadline(self)
|
85
konova/forms/modals/resubmission_form.py
Normal file
85
konova/forms/modals/resubmission_form.py
Normal file
@ -0,0 +1,85 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from django import forms
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import transaction
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.forms.modals.base_form import BaseModalForm
|
||||
from konova.models import Resubmission
|
||||
|
||||
|
||||
class ResubmissionModalForm(BaseModalForm):
|
||||
date = forms.DateField(
|
||||
label_suffix=_(""),
|
||||
label=_("Date"),
|
||||
help_text=_("When do you want to be reminded?"),
|
||||
widget=forms.DateInput(
|
||||
attrs={
|
||||
"type": "date",
|
||||
"data-provide": "datepicker",
|
||||
"class": "form-control",
|
||||
},
|
||||
format="%d.%m.%Y"
|
||||
)
|
||||
)
|
||||
comment = forms.CharField(
|
||||
required=False,
|
||||
label=_("Comment"),
|
||||
label_suffix=_(""),
|
||||
help_text=_("Additional comment"),
|
||||
widget=forms.Textarea(
|
||||
attrs={
|
||||
"cols": 30,
|
||||
"rows": 5,
|
||||
"class": "form-control",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Resubmission")
|
||||
self.form_caption = _("Set your resubmission for this entry.")
|
||||
self.action_url = None
|
||||
|
||||
try:
|
||||
self.resubmission = self.instance.resubmissions.get(
|
||||
user=self.user
|
||||
)
|
||||
self.initialize_form_field("date", str(self.resubmission.resubmit_on))
|
||||
self.initialize_form_field("comment", self.resubmission.comment)
|
||||
except ObjectDoesNotExist:
|
||||
self.resubmission = Resubmission()
|
||||
|
||||
def is_valid(self):
|
||||
super_valid = super().is_valid()
|
||||
self_valid = True
|
||||
|
||||
date = self.cleaned_data.get("date")
|
||||
today = datetime.date.today()
|
||||
if date <= today:
|
||||
self.add_error(
|
||||
"date",
|
||||
_("The date should be in the future")
|
||||
)
|
||||
self_valid = False
|
||||
|
||||
return super_valid and self_valid
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
self.resubmission.user = self.user
|
||||
self.resubmission.resubmit_on = self.cleaned_data.get("date")
|
||||
self.resubmission.comment = self.cleaned_data.get("comment")
|
||||
self.resubmission.save()
|
||||
self.instance.resubmissions.add(self.resubmission)
|
||||
return self.resubmission
|
||||
|
54
konova/forms/remove_form.py
Normal file
54
konova/forms/remove_form.py
Normal file
@ -0,0 +1,54 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 15.08.22
|
||||
|
||||
"""
|
||||
from django import forms
|
||||
from django.db import transaction
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.forms.base_form import BaseForm
|
||||
from user.models import UserActionLogEntry, User
|
||||
|
||||
|
||||
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.get_deleted_action(user)
|
||||
self.object_to_remove.deleted = action
|
||||
self.object_to_remove.save()
|
||||
return self.object_to_remove
|
@ -33,6 +33,7 @@ class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('konova', '0004_auto_20220209_0839'),
|
||||
('compensation', '0002_auto_20220114_0936'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
|
33
konova/migrations/0014_resubmission.py
Normal file
33
konova/migrations/0014_resubmission.py
Normal file
@ -0,0 +1,33 @@
|
||||
# Generated by Django 3.1.3 on 2022-08-15 06:03
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
import uuid
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
('user', '0006_auto_20220815_0759'),
|
||||
('konova', '0013_auto_20220713_0814'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='Resubmission',
|
||||
fields=[
|
||||
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||
('resubmit_on', models.DateField(help_text='On which date the resubmission should be performed')),
|
||||
('resubmission_sent', models.BooleanField(default=False, help_text='Whether a resubmission has been sent or not')),
|
||||
('comment', models.TextField(blank=True, help_text='Optional comment for the user itself', null=True)),
|
||||
('created', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='user.useractionlogentry')),
|
||||
('modified', models.ForeignKey(blank=True, help_text='Last modified', null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='user.useractionlogentry')),
|
||||
('user', models.ForeignKey(help_text='The user who wants to be notifed', on_delete=django.db.models.deletion.CASCADE, to=settings.AUTH_USER_MODEL)),
|
||||
],
|
||||
options={
|
||||
'abstract': False,
|
||||
},
|
||||
),
|
||||
]
|
@ -749,7 +749,6 @@ class GeoReferencedMixin(models.Model):
|
||||
class ResubmitableObjectMixin(models.Model):
|
||||
resubmissions = models.ManyToManyField(
|
||||
"konova.Resubmission",
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name="+",
|
||||
)
|
||||
|
@ -5,10 +5,9 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 01.09.21
|
||||
|
||||
"""
|
||||
from django.http import FileResponse, HttpRequest, HttpResponse, Http404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.http import FileResponse, HttpRequest, Http404
|
||||
|
||||
from konova.forms import RemoveModalForm
|
||||
from konova.forms.modals import RemoveModalForm
|
||||
from konova.models import AbstractDocument
|
||||
from konova.utils.message_templates import DOCUMENT_REMOVED_TEMPLATE
|
||||
|
||||
|
@ -15,7 +15,8 @@ from api.models import APIUserToken
|
||||
from intervention.inputs import GenerateInput
|
||||
from user.models import User, UserNotification, Team
|
||||
|
||||
from konova.forms import BaseForm, BaseModalForm, RemoveModalForm
|
||||
from konova.forms.modals import BaseModalForm, RemoveModalForm
|
||||
from konova.forms import BaseForm
|
||||
|
||||
|
||||
class UserNotificationForm(BaseForm):
|
||||
|
18
user/migrations/0006_auto_20220815_0759.py
Normal file
18
user/migrations/0006_auto_20220815_0759.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 3.1.3 on 2022-08-15 05:59
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('user', '0005_team_deleted'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='usernotification',
|
||||
name='id',
|
||||
field=models.CharField(choices=[('NOTIFY_ON_SHARED_ACCESS_REMOVED', 'NOTIFY_ON_SHARED_ACCESS_REMOVED'), ('NOTIFY_ON_SHARED_DATA_RECORDED', 'NOTIFY_ON_SHARED_DATA_RECORDED'), ('NOTIFY_ON_SHARED_DATA_DELETED', 'NOTIFY_ON_SHARED_DATA_DELETED'), ('NOTIFY_ON_SHARED_DATA_CHECKED', 'NOTIFY_ON_SHARED_DATA_CHECKED'), ('NOTIFY_ON_SHARED_ACCESS_GAINED', 'NOTIFY_ON_SHARED_ACCESS_GAINED'), ('NOTIFY_ON_DEDUCTION_CHANGES', 'NOTIFY_ON_DEDUCTION_CHANGES')], max_length=500, primary_key=True, serialize=False),
|
||||
),
|
||||
]
|
Loading…
Reference in New Issue
Block a user