diff --git a/compensation/forms/modalForms.py b/compensation/forms/modalForms.py
index 581a7b3a..ef21aa3f 100644
--- a/compensation/forms/modalForms.py
+++ b/compensation/forms/modalForms.py
@@ -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
diff --git a/compensation/migrations/0008_auto_20220815_0803.py b/compensation/migrations/0008_auto_20220815_0803.py
new file mode 100644
index 00000000..a4d63132
--- /dev/null
+++ b/compensation/migrations/0008_auto_20220815_0803.py
@@ -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'),
+ ),
+ ]
diff --git a/compensation/migrations/0009_auto_20220815_0803.py b/compensation/migrations/0009_auto_20220815_0803.py
new file mode 100644
index 00000000..a7c00e60
--- /dev/null
+++ b/compensation/migrations/0009_auto_20220815_0803.py
@@ -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'),
+ ),
+ ]
diff --git a/compensation/migrations/0010_auto_20220815_1030.py b/compensation/migrations/0010_auto_20220815_1030.py
new file mode 100644
index 00000000..2d3f16e2
--- /dev/null
+++ b/compensation/migrations/0010_auto_20220815_1030.py
@@ -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'),
+ ),
+ ]
diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py
index e513c95c..b65d259e 100644
--- a/compensation/models/compensation.py
+++ b/compensation/models/compensation.py
@@ -19,14 +19,17 @@ from compensation.managers import CompensationManager
from compensation.models import CompensationState, CompensationAction
from compensation.utils.quality import CompensationQualityChecker
from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \
- GeoReferencedMixin, DeadlineType
+ GeoReferencedMixin, DeadlineType, ResubmitableObjectMixin
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \
DOCUMENT_REMOVED_TEMPLATE, DEADLINE_REMOVED, ADDED_DEADLINE, \
COMPENSATION_ACTION_REMOVED, COMPENSATION_STATE_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
from user.models import UserActionLogEntry
-class AbstractCompensation(BaseObject, GeoReferencedMixin):
+class AbstractCompensation(BaseObject,
+ GeoReferencedMixin,
+ ResubmitableObjectMixin
+ ):
"""
Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation,
EMA or EcoAccount.
diff --git a/compensation/templates/compensation/detail/compensation/includes/controls.html b/compensation/templates/compensation/detail/compensation/includes/controls.html
index 5be0b3e8..4119480e 100644
--- a/compensation/templates/compensation/detail/compensation/includes/controls.html
+++ b/compensation/templates/compensation/detail/compensation/includes/controls.html
@@ -12,6 +12,9 @@
{% if has_access %}
+
{% if is_default_member %}
{% if has_access %}
+
diff --git a/compensation/urls/compensation.py b/compensation/urls/compensation.py
index e1a41ff2..66020055 100644
--- a/compensation/urls/compensation.py
+++ b/compensation/urls/compensation.py
@@ -31,6 +31,7 @@ urlpatterns = [
path('/deadline//edit', deadline_edit_view, name='deadline-edit'),
path('/deadline//remove', deadline_remove_view, name='deadline-remove'),
path('/report', report_view, name='report'),
+ path('/resub', create_resubmission_view, name='resubmission-create'),
# Documents
path('/document/new/', new_document_view, name='new-doc'),
diff --git a/compensation/urls/eco_account.py b/compensation/urls/eco_account.py
index a3d1aa38..5a84e8ca 100644
--- a/compensation/urls/eco_account.py
+++ b/compensation/urls/eco_account.py
@@ -19,6 +19,7 @@ urlpatterns = [
path('/report', report_view, name='report'),
path('/edit', edit_view, name='edit'),
path('/remove', remove_view, name='remove'),
+ path('/resub', create_resubmission_view, name='resubmission-create'),
path('/state/new', state_new_view, name='new-state'),
path('/state//edit', state_edit_view, name='state-edit'),
diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py
index efe51ce3..db01a045 100644
--- a/compensation/views/compensation.py
+++ b/compensation/views/compensation.py
@@ -14,7 +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
@@ -656,3 +658,26 @@ def report_view(request: HttpRequest, id: str):
}
context = BaseContext(request, context).context
return render(request, template, context)
+
+
+@login_required
+@default_group_required
+@shared_access_required(Compensation, "id")
+def create_resubmission_view(request: HttpRequest, id: str):
+ """ Renders resubmission form for a compensation
+
+ Args:
+ request (HttpRequest): The incoming request
+ id (str): Compensation's id
+
+ Returns:
+
+ """
+ com = get_object_or_404(Compensation, id=id)
+ form = ResubmissionModalForm(request.POST or None, instance=com, request=request)
+ form.action_url = reverse("compensation:resubmission-create", args=(id,))
+ return form.process_request(
+ request,
+ msg_success=_("Resubmission set"),
+ redirect_url=reverse("compensation:detail", args=(id,))
+ )
diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py
index ebface8d..2ebeb1f7 100644
--- a/compensation/views/eco_account.py
+++ b/compensation/views/eco_account.py
@@ -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, \
- RemoveDeadlineModalForm, EditDocumentModalForm
+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, \
@@ -838,4 +839,27 @@ def create_share_view(request: HttpRequest, id: str):
return form.process_request(
request,
msg_success=_("Share settings updated")
+ )
+
+
+@login_required
+@default_group_required
+@shared_access_required(EcoAccount, "id")
+def create_resubmission_view(request: HttpRequest, id: str):
+ """ Renders resubmission form for an eco account
+
+ Args:
+ request (HttpRequest): The incoming request
+ id (str): EcoAccount's id
+
+ Returns:
+
+ """
+ acc = get_object_or_404(EcoAccount, id=id)
+ form = ResubmissionModalForm(request.POST or None, instance=acc, request=request)
+ form.action_url = reverse("compensation:acc:resubmission-create", args=(id,))
+ return form.process_request(
+ request,
+ msg_success=_("Resubmission set"),
+ redirect_url=reverse("compensation:acc:detail", args=(id,))
)
\ No newline at end of file
diff --git a/compensation/views/payment.py b/compensation/views/payment.py
index 2be5455e..84fad5bc 100644
--- a/compensation/views/payment.py
+++ b/compensation/views/payment.py
@@ -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
diff --git a/ema/forms.py b/ema/forms.py
index a7e82c4f..93f23490 100644
--- a/ema/forms.py
+++ b/ema/forms.py
@@ -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
diff --git a/ema/migrations/0005_ema_resubmission.py b/ema/migrations/0005_ema_resubmission.py
new file mode 100644
index 00000000..57a1fbe7
--- /dev/null
+++ b/ema/migrations/0005_ema_resubmission.py
@@ -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'),
+ ),
+ ]
diff --git a/ema/migrations/0006_auto_20220815_0803.py b/ema/migrations/0006_auto_20220815_0803.py
new file mode 100644
index 00000000..44ae7657
--- /dev/null
+++ b/ema/migrations/0006_auto_20220815_0803.py
@@ -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'),
+ ),
+ ]
diff --git a/ema/migrations/0007_auto_20220815_1030.py b/ema/migrations/0007_auto_20220815_1030.py
new file mode 100644
index 00000000..84429174
--- /dev/null
+++ b/ema/migrations/0007_auto_20220815_1030.py
@@ -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'),
+ ),
+ ]
diff --git a/ema/templates/ema/detail/includes/controls.html b/ema/templates/ema/detail/includes/controls.html
index 6a4f7062..a16071bf 100644
--- a/ema/templates/ema/detail/includes/controls.html
+++ b/ema/templates/ema/detail/includes/controls.html
@@ -12,6 +12,9 @@
{% if has_access %}
+
diff --git a/ema/urls.py b/ema/urls.py
index 90cafb66..63073d6e 100644
--- a/ema/urls.py
+++ b/ema/urls.py
@@ -19,6 +19,7 @@ urlpatterns = [
path('/remove', remove_view, name='remove'),
path('/record', record_view, name='record'),
path('/report', report_view, name='report'),
+ path('/resub', create_resubmission_view, name='resubmission-create'),
path('/state/new', state_new_view, name='new-state'),
path('/state//remove', state_remove_view, name='state-remove'),
diff --git a/ema/views.py b/ema/views.py
index 589165f5..f07187aa 100644
--- a/ema/views.py
+++ b/ema/views.py
@@ -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, \
- EditDocumentModalForm
+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
@@ -710,4 +711,27 @@ def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
request,
msg_success=DEADLINE_REMOVED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
- )
\ No newline at end of file
+ )
+
+
+@login_required
+@conservation_office_group_required
+@shared_access_required(Ema, "id")
+def create_resubmission_view(request: HttpRequest, id: str):
+ """ Renders resubmission form for an EMA
+
+ Args:
+ request (HttpRequest): The incoming request
+ id (str): EMA's id
+
+ Returns:
+
+ """
+ ema = get_object_or_404(Ema, id=id)
+ form = ResubmissionModalForm(request.POST or None, instance=ema, request=request)
+ form.action_url = reverse("ema:resubmission-create", args=(id,))
+ return form.process_request(
+ request,
+ msg_success=_("Resubmission set"),
+ redirect_url=reverse("ema:detail", args=(id,))
+ )
diff --git a/intervention/forms/forms.py b/intervention/forms/forms.py
index b85ba101..15b02fd7 100644
--- a/intervention/forms/forms.py
+++ b/intervention/forms/forms.py
@@ -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
diff --git a/intervention/forms/modalForms.py b/intervention/forms/modalForms.py
index b6445a55..a977c1ce 100644
--- a/intervention/forms/modalForms.py
+++ b/intervention/forms/modalForms.py
@@ -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
diff --git a/intervention/migrations/0005_intervention_resubmission.py b/intervention/migrations/0005_intervention_resubmission.py
new file mode 100644
index 00000000..ac489238
--- /dev/null
+++ b/intervention/migrations/0005_intervention_resubmission.py
@@ -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'),
+ ),
+ ]
diff --git a/intervention/migrations/0006_auto_20220815_0803.py b/intervention/migrations/0006_auto_20220815_0803.py
new file mode 100644
index 00000000..8d0bf80d
--- /dev/null
+++ b/intervention/migrations/0006_auto_20220815_0803.py
@@ -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'),
+ ),
+ ]
diff --git a/intervention/migrations/0007_auto_20220815_1030.py b/intervention/migrations/0007_auto_20220815_1030.py
new file mode 100644
index 00000000..b7a2729d
--- /dev/null
+++ b/intervention/migrations/0007_auto_20220815_1030.py
@@ -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'),
+ ),
+ ]
diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py
index dd15beb1..ea561c5b 100644
--- a/intervention/models/intervention.py
+++ b/intervention/models/intervention.py
@@ -26,14 +26,19 @@ from intervention.models.revocation import RevocationDocument, Revocation
from intervention.utils.quality import InterventionQualityChecker
from konova.models import generate_document_file_upload_path, AbstractDocument, BaseObject, \
ShareableObjectMixin, \
- RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin
-from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP
+ RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin, ResubmitableObjectMixin
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE, \
PAYMENT_REMOVED, PAYMENT_ADDED, REVOCATION_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
from user.models import UserActionLogEntry
-class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin):
+class Intervention(BaseObject,
+ ShareableObjectMixin,
+ RecordableObjectMixin,
+ CheckableObjectMixin,
+ GeoReferencedMixin,
+ ResubmitableObjectMixin
+ ):
"""
Interventions are e.g. construction sites where nature used to be.
"""
diff --git a/intervention/templates/intervention/detail/includes/controls.html b/intervention/templates/intervention/detail/includes/controls.html
index f41c8b85..7af2165b 100644
--- a/intervention/templates/intervention/detail/includes/controls.html
+++ b/intervention/templates/intervention/detail/includes/controls.html
@@ -12,6 +12,9 @@
{% if has_access %}
+
diff --git a/intervention/urls.py b/intervention/urls.py
index 2a5e6d38..c7c43837 100644
--- a/intervention/urls.py
+++ b/intervention/urls.py
@@ -10,7 +10,8 @@ from django.urls import path
from intervention.views import index_view, new_view, detail_view, edit_view, remove_view, new_document_view, share_view, \
create_share_view, remove_revocation_view, new_revocation_view, check_view, log_view, new_deduction_view, \
record_view, remove_document_view, get_document_view, get_revocation_view, new_id_view, report_view, \
- remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view
+ remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view, \
+ create_resubmission_view
app_name = "intervention"
urlpatterns = [
@@ -26,6 +27,7 @@ urlpatterns = [
path('/check', check_view, name='check'),
path('/record', record_view, name='record'),
path('/report', report_view, name='report'),
+ path('/resub', create_resubmission_view, name='resubmission-create'),
# Compensations
path('/compensation//remove', remove_compensation_view, name='remove-compensation'),
diff --git a/intervention/views.py b/intervention/views.py
index 36577202..6a9304e9 100644
--- a/intervention/views.py
+++ b/intervention/views.py
@@ -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
+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
@@ -475,6 +476,29 @@ def create_share_view(request: HttpRequest, id: str):
)
+@login_required
+@default_group_required
+@shared_access_required(Intervention, "id")
+def create_resubmission_view(request: HttpRequest, id: str):
+ """ Renders resubmission form for an intervention
+
+ Args:
+ request (HttpRequest): The incoming request
+ id (str): Intervention's id
+
+ Returns:
+
+ """
+ intervention = get_object_or_404(Intervention, id=id)
+ form = ResubmissionModalForm(request.POST or None, instance=intervention, request=request)
+ form.action_url = reverse("intervention:resubmission-create", args=(id,))
+ return form.process_request(
+ request,
+ msg_success=_("Resubmission set"),
+ redirect_url=reverse("intervention:detail", args=(id,))
+ )
+
+
@login_required
@registration_office_group_required
@shared_access_required(Intervention, "id")
diff --git a/konova/admin.py b/konova/admin.py
index b30f4b14..07d692d7 100644
--- a/konova/admin.py
+++ b/konova/admin.py
@@ -7,7 +7,7 @@ Created on: 22.07.21
"""
from django.contrib import admin
-from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup
+from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup, Resubmission
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from user.models import UserAction
@@ -139,6 +139,16 @@ class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
]
+class ResubmissionAdmin(BaseResourceAdmin):
+ list_display = [
+ "resubmit_on"
+ ]
+ fields = [
+ "comment",
+ "resubmit_on",
+ "resubmission_sent",
+ ]
+
# Outcommented for a cleaner admin backend on production
#admin.site.register(Geometry, GeometryAdmin)
@@ -148,3 +158,4 @@ class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
#admin.site.register(ParcelGroup, ParcelGroupAdmin)
#admin.site.register(GeometryConflict, GeometryConflictAdmin)
#admin.site.register(Deadline, DeadlineAdmin)
+#admin.site.register(Resubmission, ResubmissionAdmin)
diff --git a/konova/forms.py b/konova/forms.py
deleted file mode 100644
index 67014260..00000000
--- a/konova/forms.py
+++ /dev/null
@@ -1,688 +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.db.models.fields.files import FieldFile
-
-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
-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,
- 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
diff --git a/konova/forms/__init__.py b/konova/forms/__init__.py
new file mode 100644
index 00000000..5840c4d2
--- /dev/null
+++ b/konova/forms/__init__.py
@@ -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 *
\ No newline at end of file
diff --git a/konova/forms/base_form.py b/konova/forms/base_form.py
new file mode 100644
index 00000000..065fba17
--- /dev/null
+++ b/konova/forms/base_form.py
@@ -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"
diff --git a/konova/forms/geometry_form.py b/konova/forms/geometry_form.py
new file mode 100644
index 00000000..3c957aa7
--- /dev/null
+++ b/konova/forms/geometry_form.py
@@ -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
\ No newline at end of file
diff --git a/konova/forms/modals/__init__.py b/konova/forms/modals/__init__.py
new file mode 100644
index 00000000..f922f2de
--- /dev/null
+++ b/konova/forms/modals/__init__.py
@@ -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 *
diff --git a/konova/forms/modals/base_form.py b/konova/forms/modals/base_form.py
new file mode 100644
index 00000000..a6806578
--- /dev/null
+++ b/konova/forms/modals/base_form.py
@@ -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
diff --git a/konova/forms/modals/document_form.py b/konova/forms/modals/document_form.py
new file mode 100644
index 00000000..96b4f8e8
--- /dev/null
+++ b/konova/forms/modals/document_form.py
@@ -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
+
diff --git a/konova/forms/modals/record_form.py b/konova/forms/modals/record_form.py
new file mode 100644
index 00000000..812b697a
--- /dev/null
+++ b/konova/forms/modals/record_form.py
@@ -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
diff --git a/konova/forms/modals/remove_form.py b/konova/forms/modals/remove_form.py
new file mode 100644
index 00000000..7a146268
--- /dev/null
+++ b/konova/forms/modals/remove_form.py
@@ -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)
\ No newline at end of file
diff --git a/konova/forms/modals/resubmission_form.py b/konova/forms/modals/resubmission_form.py
new file mode 100644
index 00000000..d1d846f6
--- /dev/null
+++ b/konova/forms/modals/resubmission_form.py
@@ -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
+
diff --git a/konova/forms/remove_form.py b/konova/forms/remove_form.py
new file mode 100644
index 00000000..d5c884a6
--- /dev/null
+++ b/konova/forms/remove_form.py
@@ -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
diff --git a/konova/management/commands/handle_resubmissions.py b/konova/management/commands/handle_resubmissions.py
new file mode 100644
index 00000000..047dbd5c
--- /dev/null
+++ b/konova/management/commands/handle_resubmissions.py
@@ -0,0 +1,46 @@
+"""
+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 compensation.models import Compensation, EcoAccount
+from ema.models import Ema
+from intervention.models import Intervention
+from konova.management.commands.setup import BaseKonovaCommand
+from konova.models import Resubmission
+
+
+class Command(BaseKonovaCommand):
+ help = "Checks for resubmissions due now"
+
+ def handle(self, *args, **options):
+ try:
+ resubmitable_models = [
+ Intervention,
+ Compensation,
+ Ema,
+ EcoAccount,
+ ]
+ today = datetime.date.today()
+ resubmissions = Resubmission.objects.filter(
+ resubmit_on__lte=today,
+ resubmission_sent=False,
+ )
+ self._write_warning(f"Found {resubmissions.count()} resubmission. Process now...")
+ for model in resubmitable_models:
+ all_objs = model.objects.filter(
+ resubmissions__in=resubmissions
+ )
+ self._write_warning(f"Process resubmissions for {all_objs.count()} {model.__name__} entries")
+ for obj in all_objs:
+ obj.resubmit()
+ self._write_success("Mails have been sent.")
+ resubmissions.delete()
+ self._write_success("Resubmissions have been deleted.")
+ except KeyboardInterrupt:
+ self._break_line()
+ exit(-1)
\ No newline at end of file
diff --git a/konova/migrations/0005_auto_20220216_0856.py b/konova/migrations/0005_auto_20220216_0856.py
index 567e2065..8626b7c5 100644
--- a/konova/migrations/0005_auto_20220216_0856.py
+++ b/konova/migrations/0005_auto_20220216_0856.py
@@ -33,6 +33,7 @@ class Migration(migrations.Migration):
dependencies = [
('konova', '0004_auto_20220209_0839'),
+ ('compensation', '0002_auto_20220114_0936'),
]
operations = [
diff --git a/konova/migrations/0014_resubmission.py b/konova/migrations/0014_resubmission.py
new file mode 100644
index 00000000..f0ef9e7e
--- /dev/null
+++ b/konova/migrations/0014_resubmission.py
@@ -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,
+ },
+ ),
+ ]
diff --git a/konova/models/__init__.py b/konova/models/__init__.py
index c9156061..ba9de1d5 100644
--- a/konova/models/__init__.py
+++ b/konova/models/__init__.py
@@ -10,3 +10,4 @@ from .deadline import *
from .document import *
from .geometry import *
from .parcel import *
+from .resubmission import *
diff --git a/konova/models/object.py b/konova/models/object.py
index b468932a..8af95e18 100644
--- a/konova/models/object.py
+++ b/konova/models/object.py
@@ -743,4 +743,23 @@ class GeoReferencedMixin(models.Model):
zoom_lvl,
x,
y,
- )
\ No newline at end of file
+ )
+
+
+class ResubmitableObjectMixin(models.Model):
+ resubmissions = models.ManyToManyField(
+ "konova.Resubmission",
+ blank=True,
+ related_name="+",
+ )
+
+ class Meta:
+ abstract = True
+
+ def resubmit(self):
+ """ Run resubmit check and run for all related resubmissions
+
+ """
+ resubmissions = self.resubmissions.all()
+ for resubmission in resubmissions:
+ resubmission.send_resubmission_mail(self.identifier)
diff --git a/konova/models/resubmission.py b/konova/models/resubmission.py
new file mode 100644
index 00000000..be5fe842
--- /dev/null
+++ b/konova/models/resubmission.py
@@ -0,0 +1,46 @@
+"""
+Author: Michel Peltriaux
+Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
+Contact: ksp-servicestelle@sgdnord.rlp.de
+Created on: 15.08.22
+
+"""
+from dateutil.utils import today
+from django.db import models
+
+from konova.models import BaseResource
+from konova.utils.mailer import Mailer
+
+
+class Resubmission(BaseResource):
+ user = models.ForeignKey(
+ "user.User",
+ on_delete=models.CASCADE,
+ help_text="The user who wants to be notifed"
+ )
+ 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(
+ null=True,
+ blank=True,
+ help_text="Optional comment for the user itself"
+ )
+
+ def send_resubmission_mail(self, obj_identifier):
+ """ Sends a resubmission mail
+
+ """
+ _today = today().date()
+ resubmission_handled = _today.__ge__(self.resubmit_on) and self.resubmission_sent
+ if resubmission_handled:
+ return
+
+ mailer = Mailer()
+ mailer.send_mail_resubmission(obj_identifier, self)
+ self.resubmission_sent = True
+ self.save()
diff --git a/konova/utils/documents.py b/konova/utils/documents.py
index f9b15160..3e8f6f12 100644
--- a/konova/utils/documents.py
+++ b/konova/utils/documents.py
@@ -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
diff --git a/konova/utils/mailer.py b/konova/utils/mailer.py
index 92bd2b60..8de91198 100644
--- a/konova/utils/mailer.py
+++ b/konova/utils/mailer.py
@@ -398,3 +398,26 @@ class Mailer:
msg
)
+ def send_mail_resubmission(self, obj_identifier, resubmission):
+ """ Send a resubmission mail for a user
+
+ Args:
+ obj_identifier (str): The (resubmitted) object's identifier
+ resubmission (Resubmission): The resubmission
+
+ Returns:
+
+ """
+ context = {
+ "obj_identifier": obj_identifier,
+ "resubmission": resubmission,
+ "EMAIL_REPLY_TO": EMAIL_REPLY_TO,
+ }
+ msg = render_to_string("email/resubmission/resubmission.html", context)
+ user_mail_address = [SUPPORT_MAIL_RECIPIENT]
+ self.send(
+ user_mail_address,
+ _("Resubmission - {}").format(obj_identifier),
+ msg
+ )
+
diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo
index feda2678..03853f48 100644
Binary files a/locale/de/LC_MESSAGES/django.mo and b/locale/de/LC_MESSAGES/django.mo differ
diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po
index 59a752d0..531241e3 100644
--- a/locale/de/LC_MESSAGES/django.po
+++ b/locale/de/LC_MESSAGES/django.po
@@ -18,15 +18,16 @@
#: konova/filters/mixins.py:277 konova/filters/mixins.py:323
#: konova/filters/mixins.py:361 konova/filters/mixins.py:362
#: konova/filters/mixins.py:393 konova/filters/mixins.py:394
-#: konova/forms.py:179 konova/forms.py:281 konova/forms.py:395
-#: konova/forms.py:439 konova/forms.py:449 konova/forms.py:462
-#: konova/forms.py:474 konova/forms.py:492 user/forms.py:42
+#: konova/forms.py:183 konova/forms.py:285 konova/forms.py:399
+#: konova/forms.py:443 konova/forms.py:453 konova/forms.py:466
+#: konova/forms.py:478 konova/forms.py:496 konova/forms.py:696
+#: konova/forms.py:711 user/forms.py:42
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2022-08-10 08:37+0200\n"
+"POT-Creation-Date: 2022-08-15 09:39+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -85,7 +86,7 @@ msgstr "Bericht generieren"
msgid "Select a timespan and the desired conservation office"
msgstr "Wählen Sie die Zeitspanne und die gewünschte Eintragungsstelle"
-#: analysis/forms.py:71 konova/forms.py:227
+#: analysis/forms.py:71 konova/forms.py:231
msgid "Continue"
msgstr "Weiter"
@@ -241,7 +242,8 @@ msgstr ""
#: ema/templates/ema/detail/includes/states-after.html:36
#: ema/templates/ema/detail/includes/states-before.html:36
#: intervention/forms/modalForms.py:364
-#: templates/email/other/deduction_changed.html:29
+#: templates/email/other/deduction_changed.html:31
+#: templates/email/other/deduction_changed_team.html:31
msgid "Surface"
msgstr "Fläche"
@@ -308,7 +310,8 @@ msgstr "Typ"
#: intervention/forms/modalForms.py:382 intervention/tables.py:87
#: intervention/templates/intervention/detail/view.html:19
#: konova/templates/konova/includes/quickstart/interventions.html:4
-#: templates/email/other/deduction_changed.html:24
+#: templates/email/other/deduction_changed.html:26
+#: templates/email/other/deduction_changed_team.html:26
#: templates/navbars/navbar.html:22
msgid "Intervention"
msgstr "Eingriff"
@@ -362,7 +365,7 @@ msgstr "Automatisch generiert"
#: intervention/templates/intervention/detail/includes/documents.html:28
#: intervention/templates/intervention/detail/view.html:31
#: intervention/templates/intervention/report/report.html:12
-#: konova/forms.py:438
+#: konova/forms.py:442
msgid "Title"
msgstr "Bezeichnung"
@@ -389,12 +392,13 @@ msgstr "Kompensation XY; Flur ABC"
#: intervention/templates/intervention/detail/includes/documents.html:34
#: intervention/templates/intervention/detail/includes/payments.html:34
#: intervention/templates/intervention/detail/includes/revocation.html:38
-#: konova/forms.py:473 konova/templates/konova/includes/comment_card.html:16
+#: konova/forms.py:477 konova/forms.py:710
+#: konova/templates/konova/includes/comment_card.html:16
msgid "Comment"
msgstr "Kommentar"
#: compensation/forms/forms.py:59 compensation/forms/modalForms.py:471
-#: intervention/forms/forms.py:200
+#: intervention/forms/forms.py:200 konova/forms.py:712
msgid "Additional comment"
msgstr "Zusätzlicher Kommentar"
@@ -479,7 +483,7 @@ msgstr "kompensiert Eingriff"
msgid "Select the intervention for which this compensation compensates"
msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist"
-#: compensation/forms/forms.py:219 compensation/views/compensation.py:110
+#: compensation/forms/forms.py:219 compensation/views/compensation.py:111
msgid "New compensation"
msgstr "Neue Kompensation"
@@ -531,7 +535,7 @@ msgid "Due on which date"
msgstr "Zahlung wird an diesem Datum erwartet"
#: compensation/forms/modalForms.py:65 compensation/forms/modalForms.py:363
-#: intervention/forms/modalForms.py:177 konova/forms.py:475
+#: intervention/forms/modalForms.py:177 konova/forms.py:479
msgid "Additional comment, maximum {} letters"
msgstr "Zusätzlicher Kommentar, maximal {} Zeichen"
@@ -576,7 +580,7 @@ msgstr "Neuer Zustand"
msgid "Insert data for the new state"
msgstr "Geben Sie die Daten des neuen Zustandes ein"
-#: compensation/forms/modalForms.py:219 konova/forms.py:229
+#: compensation/forms/modalForms.py:219 konova/forms.py:233
msgid "Object removed"
msgstr "Objekt entfernt"
@@ -602,7 +606,7 @@ msgstr "Fristart wählen"
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:36
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:36
#: ema/templates/ema/detail/includes/deadlines.html:36
-#: intervention/forms/modalForms.py:149
+#: intervention/forms/modalForms.py:149 konova/forms.py:697
msgid "Date"
msgstr "Datum"
@@ -850,24 +854,32 @@ msgstr "In LANIS öffnen"
msgid "Public report"
msgstr "Öffentlicher Bericht"
-#: compensation/templates/compensation/detail/compensation/includes/controls.html:17
-#: compensation/templates/compensation/detail/eco_account/includes/controls.html:31
-#: ema/templates/ema/detail/includes/controls.html:31
-#: intervention/templates/intervention/detail/includes/controls.html:36
+#: compensation/templates/compensation/detail/compensation/includes/controls.html:15
+#: compensation/templates/compensation/detail/eco_account/includes/controls.html:15
+#: ema/templates/ema/detail/includes/controls.html:15
+#: intervention/templates/intervention/detail/includes/controls.html:15
+#: konova/forms.py:724 templates/email/resubmission/resubmission.html:4
+msgid "Resubmission"
+msgstr "Wiedervorlage"
+
+#: compensation/templates/compensation/detail/compensation/includes/controls.html:20
+#: compensation/templates/compensation/detail/eco_account/includes/controls.html:34
+#: ema/templates/ema/detail/includes/controls.html:34
+#: intervention/templates/intervention/detail/includes/controls.html:39
msgid "Edit"
msgstr "Bearbeiten"
-#: compensation/templates/compensation/detail/compensation/includes/controls.html:21
-#: compensation/templates/compensation/detail/eco_account/includes/controls.html:35
-#: ema/templates/ema/detail/includes/controls.html:35
-#: intervention/templates/intervention/detail/includes/controls.html:40
-msgid "Show log"
-msgstr "Log anzeigen"
-
#: compensation/templates/compensation/detail/compensation/includes/controls.html:24
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:38
#: ema/templates/ema/detail/includes/controls.html:38
#: intervention/templates/intervention/detail/includes/controls.html:43
+msgid "Show log"
+msgstr "Log anzeigen"
+
+#: compensation/templates/compensation/detail/compensation/includes/controls.html:27
+#: compensation/templates/compensation/detail/eco_account/includes/controls.html:41
+#: ema/templates/ema/detail/includes/controls.html:41
+#: intervention/templates/intervention/detail/includes/controls.html:46
#: venv/lib/python3.7/site-packages/django/forms/formsets.py:391
msgid "Delete"
msgstr "Löschen"
@@ -907,7 +919,7 @@ msgstr "Dokumente"
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:14
#: ema/templates/ema/detail/includes/documents.html:14
#: intervention/templates/intervention/detail/includes/documents.html:14
-#: konova/forms.py:491
+#: konova/forms.py:495
msgid "Add new document"
msgstr "Neues Dokument hinzufügen"
@@ -915,7 +927,7 @@ msgstr "Neues Dokument hinzufügen"
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:31
#: ema/templates/ema/detail/includes/documents.html:31
#: intervention/templates/intervention/detail/includes/documents.html:31
-#: konova/forms.py:448
+#: konova/forms.py:452
msgid "Created on"
msgstr "Erstellt"
@@ -923,7 +935,7 @@ msgstr "Erstellt"
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:61
#: ema/templates/ema/detail/includes/documents.html:61
#: intervention/templates/intervention/detail/includes/documents.html:65
-#: konova/forms.py:553
+#: konova/forms.py:557
msgid "Edit document"
msgstr "Dokument bearbeiten"
@@ -1093,22 +1105,22 @@ msgstr ""
msgid "other users"
msgstr "weitere Nutzer"
-#: compensation/templates/compensation/detail/eco_account/includes/controls.html:15
-#: ema/templates/ema/detail/includes/controls.html:15
+#: compensation/templates/compensation/detail/eco_account/includes/controls.html:18
+#: ema/templates/ema/detail/includes/controls.html:18
#: intervention/forms/modalForms.py:71
-#: intervention/templates/intervention/detail/includes/controls.html:15
+#: intervention/templates/intervention/detail/includes/controls.html:18
msgid "Share"
msgstr "Freigabe"
-#: compensation/templates/compensation/detail/eco_account/includes/controls.html:20
-#: ema/templates/ema/detail/includes/controls.html:20
-#: intervention/templates/intervention/detail/includes/controls.html:25
+#: compensation/templates/compensation/detail/eco_account/includes/controls.html:23
+#: ema/templates/ema/detail/includes/controls.html:23
+#: intervention/templates/intervention/detail/includes/controls.html:28
msgid "Unrecord"
msgstr "Entzeichnen"
-#: compensation/templates/compensation/detail/eco_account/includes/controls.html:24
-#: ema/templates/ema/detail/includes/controls.html:24
-#: intervention/templates/intervention/detail/includes/controls.html:29
+#: compensation/templates/compensation/detail/eco_account/includes/controls.html:27
+#: ema/templates/ema/detail/includes/controls.html:27
+#: intervention/templates/intervention/detail/includes/controls.html:32
msgid "Record"
msgstr "Verzeichnen"
@@ -1215,29 +1227,34 @@ msgstr ""
msgid "Responsible data"
msgstr "Daten zu den verantwortlichen Stellen"
-#: compensation/views/compensation.py:53
+#: compensation/views/compensation.py:54
msgid "Compensations - Overview"
msgstr "Kompensationen - Übersicht"
-#: compensation/views/compensation.py:172 konova/utils/message_templates.py:36
+#: compensation/views/compensation.py:173 konova/utils/message_templates.py:36
msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet"
-#: compensation/views/compensation.py:182 compensation/views/eco_account.py:173
+#: compensation/views/compensation.py:183 compensation/views/eco_account.py:173
#: ema/views.py:241 intervention/views.py:338
msgid "Edit {}"
msgstr "Bearbeite {}"
-#: compensation/views/compensation.py:269 compensation/views/eco_account.py:360
-#: ema/views.py:195 intervention/views.py:542
+#: compensation/views/compensation.py:270 compensation/views/eco_account.py:360
+#: ema/views.py:195 intervention/views.py:565
msgid "Log"
msgstr "Log"
-#: compensation/views/compensation.py:613 compensation/views/eco_account.py:728
-#: ema/views.py:559 intervention/views.py:688
+#: compensation/views/compensation.py:614 compensation/views/eco_account.py:728
+#: ema/views.py:559 intervention/views.py:711
msgid "Report {}"
msgstr "Bericht {}"
+#: compensation/views/compensation.py:680 compensation/views/eco_account.py:862
+#: ema/views.py:734 intervention/views.py:496
+msgid "Resubmission set"
+msgstr "Wiedervorlage gesetzt"
+
#: compensation/views/eco_account.py:65
msgid "Eco-account - Overview"
msgstr "Ökokonten - Übersicht"
@@ -1255,12 +1272,12 @@ msgid "Eco-account removed"
msgstr "Ökokonto entfernt"
#: compensation/views/eco_account.py:381 ema/views.py:283
-#: intervention/views.py:641
+#: intervention/views.py:664
msgid "{} unrecorded"
msgstr "{} entzeichnet"
#: compensation/views/eco_account.py:381 ema/views.py:283
-#: intervention/views.py:641
+#: intervention/views.py:664
msgid "{} recorded"
msgstr "{} verzeichnet"
@@ -1462,11 +1479,11 @@ msgid "Checked compensations data and payments"
msgstr "Kompensationen und Zahlungen geprüft"
#: intervention/forms/modalForms.py:263
-#: intervention/templates/intervention/detail/includes/controls.html:19
+#: intervention/templates/intervention/detail/includes/controls.html:22
msgid "Run check"
msgstr "Prüfung vornehmen"
-#: intervention/forms/modalForms.py:264 konova/forms.py:594
+#: intervention/forms/modalForms.py:264 konova/forms.py:598
msgid ""
"I, {} {}, confirm that all necessary control steps have been performed by "
"myself."
@@ -1622,11 +1639,11 @@ msgstr "Eingriff {} bearbeitet"
msgid "{} removed"
msgstr "{} entfernt"
-#: intervention/views.py:495
+#: intervention/views.py:518
msgid "Check performed"
msgstr "Prüfung durchgeführt"
-#: intervention/views.py:646
+#: intervention/views.py:669
msgid "There are errors on this intervention:"
msgstr "Es liegen Fehler in diesem Eingriff vor:"
@@ -1711,78 +1728,90 @@ msgstr "Nach Zulassungsbehörde suchen"
msgid "Search for conservation office"
msgstr "Nch Eintragungsstelle suchen"
-#: konova/forms.py:41 templates/form/collapsable/form.html:62
+#: konova/forms.py:44 templates/form/collapsable/form.html:62
msgid "Save"
msgstr "Speichern"
-#: konova/forms.py:75
+#: konova/forms.py:78
msgid "Not editable"
msgstr "Nicht editierbar"
-#: konova/forms.py:178 konova/forms.py:394
+#: konova/forms.py:182 konova/forms.py:398
msgid "Confirm"
msgstr "Bestätige"
-#: konova/forms.py:190 konova/forms.py:403
+#: konova/forms.py:194 konova/forms.py:407
msgid "Remove"
msgstr "Löschen"
-#: konova/forms.py:192
+#: konova/forms.py:196
msgid "You are about to remove {} {}"
msgstr "Sie sind dabei {} {} zu löschen"
-#: konova/forms.py:280 konova/utils/quality.py:44 konova/utils/quality.py:46
+#: konova/forms.py:284 konova/utils/quality.py:44 konova/utils/quality.py:46
#: templates/form/collapsable/form.html:45
msgid "Geometry"
msgstr "Geometrie"
-#: konova/forms.py:331
+#: konova/forms.py:335
msgid "Only surfaces allowed. Points or lines must be buffered."
msgstr ""
"Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden."
-#: konova/forms.py:404
+#: konova/forms.py:408
msgid "Are you sure?"
msgstr "Sind Sie sicher?"
-#: konova/forms.py:450
+#: konova/forms.py:454
msgid "When has this file been created? Important for photos."
msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?"
-#: konova/forms.py:461
+#: konova/forms.py:465
#: venv/lib/python3.7/site-packages/django/db/models/fields/files.py:231
msgid "File"
msgstr "Datei"
-#: konova/forms.py:463
+#: konova/forms.py:467
msgid "Allowed formats: pdf, jpg, png. Max size 15 MB."
msgstr "Formate: pdf, jpg, png. Maximal 15 MB."
-#: konova/forms.py:528
+#: konova/forms.py:532
msgid "Added document"
msgstr "Dokument hinzugefügt"
-#: konova/forms.py:585
+#: konova/forms.py:589
msgid "Confirm record"
msgstr "Verzeichnen bestätigen"
-#: konova/forms.py:593
+#: konova/forms.py:597
msgid "Record data"
msgstr "Daten verzeichnen"
-#: konova/forms.py:600
+#: konova/forms.py:604
msgid "Confirm unrecord"
msgstr "Entzeichnen bestätigen"
-#: konova/forms.py:601
+#: konova/forms.py:605
msgid "Unrecord data"
msgstr "Daten entzeichnen"
-#: konova/forms.py:602
+#: konova/forms.py:606
msgid "I, {} {}, confirm that this data must be unrecorded."
msgstr ""
"Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen."
+#: konova/forms.py:698
+msgid "When do you want to be reminded?"
+msgstr "Wann wollen Sie erinnert werden?"
+
+#: konova/forms.py:725
+msgid "Set your resubmission for this entry."
+msgstr "Setzen Sie eine Wiedervorlage für diesen Eintrag."
+
+#: konova/forms.py:746
+msgid "The date should be in the future"
+msgstr "Das Datum sollte in der Zukunft liegen"
+
#: konova/management/commands/setup_data.py:26
msgid "On shared access gained"
msgstr "Wenn mir eine Freigabe zu Daten erteilt wird"
@@ -1929,7 +1958,7 @@ msgstr "{} - Freigegebene Daten verzeichnet"
msgid "{} - Shared data checked"
msgstr "{} - Freigegebene Daten geprüft"
-#: konova/utils/mailer.py:233 konova/utils/mailer.py:372
+#: konova/utils/mailer.py:233 konova/utils/mailer.py:376
msgid "{} - Deduction changed"
msgstr "{} - Abbuchung geändert"
@@ -1937,10 +1966,14 @@ msgstr "{} - Abbuchung geändert"
msgid "{} - Shared data deleted"
msgstr "{} - Freigegebene Daten gelöscht"
-#: konova/utils/mailer.py:393 templates/email/api/verify_token.html:4
+#: konova/utils/mailer.py:397 templates/email/api/verify_token.html:4
msgid "Request for new API token"
msgstr "Anfrage für neuen API Token"
+#: konova/utils/mailer.py:420
+msgid "Resubmission - {}"
+msgstr "Wiedervorlage - {}"
+
#: konova/utils/message_templates.py:10
msgid "no further details"
msgstr "keine weitere Angabe"
@@ -2223,11 +2256,11 @@ msgstr "Irgendetwas ist passiert. Wir arbeiten daran!"
msgid "Hello support"
msgstr "Hallo Support"
-#: templates/email/api/verify_token.html:9
+#: templates/email/api/verify_token.html:10
msgid "you need to verify the API token for user"
msgstr "Sie müssen einen API Token für folgenden Nutzer freischalten"
-#: templates/email/api/verify_token.html:15
+#: templates/email/api/verify_token.html:16
msgid ""
"If unsure, please contact the user. The API token can not be used until you "
"activated it in the admin backend."
@@ -2236,20 +2269,22 @@ msgstr ""
"Token kann so lange nicht verwendet werden, wie er noch nicht von Ihnen im "
"Admin Backend aktiviert worden ist."
-#: templates/email/api/verify_token.html:18
-#: templates/email/checking/shared_data_checked.html:19
-#: templates/email/checking/shared_data_checked_team.html:19
-#: templates/email/deleting/shared_data_deleted.html:19
-#: templates/email/deleting/shared_data_deleted_team.html:19
-#: templates/email/other/deduction_changed.html:38
-#: templates/email/recording/shared_data_recorded.html:19
-#: templates/email/recording/shared_data_recorded_team.html:19
-#: templates/email/recording/shared_data_unrecorded.html:19
-#: templates/email/recording/shared_data_unrecorded_team.html:19
-#: templates/email/sharing/shared_access_given.html:20
-#: templates/email/sharing/shared_access_given_team.html:20
-#: templates/email/sharing/shared_access_removed.html:20
-#: templates/email/sharing/shared_access_removed_team.html:20
+#: templates/email/api/verify_token.html:19
+#: templates/email/checking/shared_data_checked.html:20
+#: templates/email/checking/shared_data_checked_team.html:20
+#: templates/email/deleting/shared_data_deleted.html:20
+#: templates/email/deleting/shared_data_deleted_team.html:20
+#: templates/email/other/deduction_changed.html:41
+#: templates/email/other/deduction_changed_team.html:41
+#: templates/email/recording/shared_data_recorded.html:20
+#: templates/email/recording/shared_data_recorded_team.html:20
+#: templates/email/recording/shared_data_unrecorded.html:20
+#: templates/email/recording/shared_data_unrecorded_team.html:20
+#: templates/email/resubmission/resubmission.html:21
+#: templates/email/sharing/shared_access_given.html:21
+#: templates/email/sharing/shared_access_given_team.html:21
+#: templates/email/sharing/shared_access_removed.html:21
+#: templates/email/sharing/shared_access_removed_team.html:21
msgid "Best regards"
msgstr "Beste Grüße"
@@ -2263,18 +2298,19 @@ msgstr "Freigegebene Daten geprüft"
#: templates/email/other/deduction_changed.html:8
#: templates/email/recording/shared_data_recorded.html:8
#: templates/email/recording/shared_data_unrecorded.html:8
+#: templates/email/resubmission/resubmission.html:8
#: templates/email/sharing/shared_access_given.html:8
#: templates/email/sharing/shared_access_removed.html:8
msgid "Hello "
msgstr "Hallo "
-#: templates/email/checking/shared_data_checked.html:10
-#: templates/email/checking/shared_data_checked_team.html:10
+#: templates/email/checking/shared_data_checked.html:11
+#: templates/email/checking/shared_data_checked_team.html:11
msgid "the following dataset has just been checked"
msgstr "der folgende Datensatz wurde soeben geprüft "
-#: templates/email/checking/shared_data_checked.html:16
-#: templates/email/checking/shared_data_checked_team.html:16
+#: templates/email/checking/shared_data_checked.html:17
+#: templates/email/checking/shared_data_checked_team.html:17
msgid ""
"This means, the responsible registration office just confirmed the "
"correctness of this dataset."
@@ -2284,6 +2320,7 @@ msgstr ""
#: templates/email/checking/shared_data_checked_team.html:8
#: templates/email/deleting/shared_data_deleted_team.html:8
+#: templates/email/other/deduction_changed_team.html:8
#: templates/email/recording/shared_data_recorded_team.html:8
#: templates/email/recording/shared_data_unrecorded_team.html:8
#: templates/email/sharing/shared_access_given_team.html:8
@@ -2296,14 +2333,15 @@ msgstr "Hallo Team"
msgid "Shared data deleted"
msgstr "Freigegebene Daten gelöscht"
-#: templates/email/deleting/shared_data_deleted.html:10
-#: templates/email/deleting/shared_data_deleted_team.html:10
+#: templates/email/deleting/shared_data_deleted.html:11
+#: templates/email/deleting/shared_data_deleted_team.html:11
msgid "the following dataset has just been deleted"
msgstr "der folgende Datensatz wurde soeben gelöscht "
-#: templates/email/deleting/shared_data_deleted.html:16
-#: templates/email/deleting/shared_data_deleted_team.html:16
-#: templates/email/other/deduction_changed.html:35
+#: templates/email/deleting/shared_data_deleted.html:17
+#: templates/email/deleting/shared_data_deleted_team.html:17
+#: templates/email/other/deduction_changed.html:38
+#: templates/email/other/deduction_changed_team.html:38
msgid ""
"If this should not have been happened, please contact us. See the signature "
"for details."
@@ -2312,27 +2350,33 @@ msgstr ""
"mail Signatur finden Sie weitere Kontaktinformationen."
#: templates/email/other/deduction_changed.html:4
+#: templates/email/other/deduction_changed_team.html:4
msgid "Deduction changed"
msgstr "Abbuchung geändert"
-#: templates/email/other/deduction_changed.html:10
+#: templates/email/other/deduction_changed.html:11
+#: templates/email/other/deduction_changed_team.html:11
msgid "a deduction of this eco account has changed:"
msgstr "eine Abbuchung des Ökokontos hat sich geändert:"
-#: templates/email/other/deduction_changed.html:14
+#: templates/email/other/deduction_changed.html:16
+#: templates/email/other/deduction_changed_team.html:16
msgid "Attribute"
msgstr "Attribute"
-#: templates/email/other/deduction_changed.html:15
+#: templates/email/other/deduction_changed.html:17
+#: templates/email/other/deduction_changed_team.html:17
msgid "Old"
msgstr "Alt"
-#: templates/email/other/deduction_changed.html:16
+#: templates/email/other/deduction_changed.html:18
+#: templates/email/other/deduction_changed_team.html:18
#: templates/generic_index.html:43 user/templates/user/team/index.html:22
msgid "New"
msgstr "Neu"
-#: templates/email/other/deduction_changed.html:19
+#: templates/email/other/deduction_changed.html:21
+#: templates/email/other/deduction_changed_team.html:21
msgid "EcoAccount"
msgstr "Ökokonto"
@@ -2341,19 +2385,19 @@ msgstr "Ökokonto"
msgid "Shared data recorded"
msgstr "Freigegebene Daten verzeichnet"
-#: templates/email/recording/shared_data_recorded.html:10
-#: templates/email/recording/shared_data_recorded_team.html:10
+#: templates/email/recording/shared_data_recorded.html:11
+#: templates/email/recording/shared_data_recorded_team.html:11
msgid "the following dataset has just been recorded"
msgstr "der folgende Datensatz wurde soeben verzeichnet "
-#: templates/email/recording/shared_data_recorded.html:16
-#: templates/email/recording/shared_data_recorded_team.html:16
+#: templates/email/recording/shared_data_recorded.html:17
+#: templates/email/recording/shared_data_recorded_team.html:17
msgid "This means the data is now publicly available, e.g. in LANIS"
msgstr ""
"Das bedeutet, dass die Daten nun öffentlich verfügbar sind, z.B. im LANIS."
-#: templates/email/recording/shared_data_recorded.html:26
-#: templates/email/recording/shared_data_recorded_team.html:26
+#: templates/email/recording/shared_data_recorded.html:27
+#: templates/email/recording/shared_data_recorded_team.html:27
msgid ""
"Please note: Recorded intervention means the compensations are recorded as "
"well."
@@ -2366,18 +2410,18 @@ msgstr ""
msgid "Shared data unrecorded"
msgstr "Freigegebene Daten entzeichnet"
-#: templates/email/recording/shared_data_unrecorded.html:10
-#: templates/email/recording/shared_data_unrecorded_team.html:10
+#: templates/email/recording/shared_data_unrecorded.html:11
+#: templates/email/recording/shared_data_unrecorded_team.html:11
msgid "the following dataset has just been unrecorded"
msgstr "der folgende Datensatz wurde soeben entzeichnet "
-#: templates/email/recording/shared_data_unrecorded.html:16
-#: templates/email/recording/shared_data_unrecorded_team.html:16
+#: templates/email/recording/shared_data_unrecorded.html:17
+#: templates/email/recording/shared_data_unrecorded_team.html:17
msgid "This means the data is no longer publicly available."
msgstr "Das bedeutet, dass die Daten nicht länger öffentlich verfügbar sind."
-#: templates/email/recording/shared_data_unrecorded.html:26
-#: templates/email/recording/shared_data_unrecorded_team.html:26
+#: templates/email/recording/shared_data_unrecorded.html:27
+#: templates/email/recording/shared_data_unrecorded_team.html:27
msgid ""
"Please note: Unrecorded intervention means the compensations are unrecorded "
"as well."
@@ -2385,22 +2429,30 @@ msgstr ""
"Bitte beachten Sie: Entzeichnete Eingriffe bedeuten, dass auch die "
"zugehörigen Kompensationen automatisch entzeichnet worden sind."
+#: templates/email/resubmission/resubmission.html:11
+msgid "you wanted to be reminded on this entry."
+msgstr "Sie wollten an diesen Eintrag erinnert werden."
+
+#: templates/email/resubmission/resubmission.html:15
+msgid "Your personal comment:"
+msgstr "Ihr Kommentar:"
+
#: templates/email/sharing/shared_access_given.html:4
#: templates/email/sharing/shared_access_given_team.html:4
msgid "Access shared"
msgstr "Zugriff freigegeben"
-#: templates/email/sharing/shared_access_given.html:10
+#: templates/email/sharing/shared_access_given.html:11
msgid "the following dataset has just been shared with you"
msgstr "der folgende Datensatz wurde soeben für Sie freigegeben "
-#: templates/email/sharing/shared_access_given.html:16
-#: templates/email/sharing/shared_access_given_team.html:16
+#: templates/email/sharing/shared_access_given.html:17
+#: templates/email/sharing/shared_access_given_team.html:17
msgid "This means you can now edit this dataset."
msgstr "Das bedeutet, dass Sie diesen Datensatz nun auch bearbeiten können."
-#: templates/email/sharing/shared_access_given.html:17
-#: templates/email/sharing/shared_access_given_team.html:17
+#: templates/email/sharing/shared_access_given.html:18
+#: templates/email/sharing/shared_access_given_team.html:18
msgid ""
"The shared dataset appears now by default on your overview for this dataset "
"type."
@@ -2408,8 +2460,8 @@ msgstr ""
"Der freigegebene Datensatz ist nun standardmäßig in Ihrer Übersicht für den "
"Datensatztyp im KSP gelistet."
-#: templates/email/sharing/shared_access_given.html:27
-#: templates/email/sharing/shared_access_given_team.html:27
+#: templates/email/sharing/shared_access_given.html:28
+#: templates/email/sharing/shared_access_given_team.html:28
msgid ""
"Please note: Shared access on an intervention means you automatically have "
"editing access to related compensations."
@@ -2418,7 +2470,7 @@ msgstr ""
"Sie automatisch auch Zugriff auf die zugehörigen Kompensationen erhalten "
"haben."
-#: templates/email/sharing/shared_access_given_team.html:10
+#: templates/email/sharing/shared_access_given_team.html:11
msgid "the following dataset has just been shared with your team"
msgstr "der folgende Datensatz wurde soeben für Ihr Team freigegeben "
@@ -2427,20 +2479,20 @@ msgstr "der folgende Datensatz wurde soeben für Ihr Team freigegeben "
msgid "Shared access removed"
msgstr "Freigegebener Zugriff entzogen"
-#: templates/email/sharing/shared_access_removed.html:10
+#: templates/email/sharing/shared_access_removed.html:11
msgid ""
"your shared access, including editing, has been revoked for the dataset "
msgstr ""
"Ihnen wurde soeben der bearbeitende Zugriff auf den folgenden Datensatz "
"entzogen: "
-#: templates/email/sharing/shared_access_removed.html:16
-#: templates/email/sharing/shared_access_removed_team.html:16
+#: templates/email/sharing/shared_access_removed.html:17
+#: templates/email/sharing/shared_access_removed_team.html:17
msgid "However, you are still able to view the dataset content."
msgstr "Sie können den Datensatz aber immer noch im KSP einsehen."
-#: templates/email/sharing/shared_access_removed.html:17
-#: templates/email/sharing/shared_access_removed_team.html:17
+#: templates/email/sharing/shared_access_removed.html:18
+#: templates/email/sharing/shared_access_removed_team.html:18
msgid ""
"Please use the provided search filter on the dataset`s overview pages to "
"find them."
@@ -2448,7 +2500,7 @@ msgstr ""
"Nutzen Sie hierzu einfach die entsprechenden Suchfilter auf den "
"Übersichtsseiten"
-#: templates/email/sharing/shared_access_removed_team.html:10
+#: templates/email/sharing/shared_access_removed_team.html:11
msgid ""
"your teams shared access, including editing, has been revoked for the "
"dataset "
diff --git a/templates/email/resubmission/resubmission.html b/templates/email/resubmission/resubmission.html
new file mode 100644
index 00000000..25848f55
--- /dev/null
+++ b/templates/email/resubmission/resubmission.html
@@ -0,0 +1,29 @@
+{% load i18n %}
+
+
+
{% trans 'Resubmission' %}
+
{{obj_identifier}}
+
+
+ {% trans 'Hello ' %} {{resubmission.user.username}},
+
+
+ {% trans 'you wanted to be reminded on this entry.' %}
+
+ {% if resubmission.comment %}
+
+ {% trans 'Your personal comment:' %}
+
+ "{{resubmission.comment}}"
+ {% endif %}
+
+
+ {% trans 'Best regards' %}
+
+ KSP
+
+
+ {% include 'email/signature.html' %}
+
+
+
diff --git a/user/forms.py b/user/forms.py
index 12688b46..0649eaf3 100644
--- a/user/forms.py
+++ b/user/forms.py
@@ -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):
diff --git a/user/migrations/0006_auto_20220815_0759.py b/user/migrations/0006_auto_20220815_0759.py
new file mode 100644
index 00000000..53861083
--- /dev/null
+++ b/user/migrations/0006_auto_20220815_0759.py
@@ -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),
+ ),
+ ]