From 7e05d05d97cb604bf0128b4b3a4814bd201287b9 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 15 Aug 2022 08:08:15 +0200 Subject: [PATCH 1/4] Model * adds new model and mixin * adds new functionality for Mailer class for sending resubmission mails --- compensation/models/compensation.py | 7 ++- intervention/models/intervention.py | 11 +++-- konova/models/__init__.py | 1 + konova/models/object.py | 22 ++++++++- konova/models/resubmission.py | 46 +++++++++++++++++++ konova/utils/mailer.py | 23 ++++++++++ .../email/resubmission/resubmission.html | 29 ++++++++++++ 7 files changed, 133 insertions(+), 6 deletions(-) create mode 100644 konova/models/resubmission.py create mode 100644 templates/email/resubmission/resubmission.html diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index e513c95..b65d259 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/intervention/models/intervention.py b/intervention/models/intervention.py index dd15beb..ea561c5 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/konova/models/__init__.py b/konova/models/__init__.py index c915606..ba9de1d 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 b468932..0fbd6e8 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -743,4 +743,24 @@ class GeoReferencedMixin(models.Model): zoom_lvl, x, y, - ) \ No newline at end of file + ) + + +class ResubmitableObjectMixin(models.Model): + resubmissions = models.ManyToManyField( + "konova.Resubmission", + null=True, + 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 0000000..ca97ebb --- /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() + 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/mailer.py b/konova/utils/mailer.py index 92bd2b6..8de9119 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/templates/email/resubmission/resubmission.html b/templates/email/resubmission/resubmission.html new file mode 100644 index 0000000..25848f5 --- /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' %} +
+
+ From 60867fdf392671bd07b3caa49610efadc6327e3b Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 15 Aug 2022 09:38:51 +0200 Subject: [PATCH 2/4] Templates + Routes * adds control button for Intervention, Compensation, Ema and EcoAccount for setting a resubmission on an entry --- .../compensation/includes/controls.html | 3 + .../detail/eco_account/includes/controls.html | 3 + compensation/urls/compensation.py | 1 + compensation/urls/eco_account.py | 1 + compensation/views/compensation.py | 26 ++++++- compensation/views/eco_account.py | 25 ++++++- .../ema/detail/includes/controls.html | 3 + ema/urls.py | 1 + ema/views.py | 27 ++++++- .../detail/includes/controls.html | 3 + intervention/urls.py | 4 +- intervention/views.py | 25 ++++++- konova/admin.py | 12 ++- konova/forms.py | 74 ++++++++++++++++++- 14 files changed, 200 insertions(+), 8 deletions(-) diff --git a/compensation/templates/compensation/detail/compensation/includes/controls.html b/compensation/templates/compensation/detail/compensation/includes/controls.html index 5be0b3e..4119480 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 e1a41ff..6602005 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 a3d1aa3..5a84e8c 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 efe51ce..016f8ea 100644 --- a/compensation/views/compensation.py +++ b/compensation/views/compensation.py @@ -14,7 +14,8 @@ 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 import RemoveModalForm, SimpleGeomForm, RemoveDeadlineModalForm, EditDocumentModalForm, \ + ResubmissionModalForm 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 +657,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 ebface8..03109a8 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -26,7 +26,7 @@ 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 + RemoveDeadlineModalForm, EditDocumentModalForm, ResubmissionModalForm 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 @@ -838,4 +838,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/ema/templates/ema/detail/includes/controls.html b/ema/templates/ema/detail/includes/controls.html index 6a4f706..a16071b 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 90cafb6..63073d6 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 589165f..9cd6dd9 100644 --- a/ema/views.py +++ b/ema/views.py @@ -17,7 +17,7 @@ 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 + EditDocumentModalForm, ResubmissionModalForm 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 +710,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/templates/intervention/detail/includes/controls.html b/intervention/templates/intervention/detail/includes/controls.html index f41c8b8..7af2165 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 2a5e6d3..c7c4383 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 3657720..c55fe72 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -12,7 +12,7 @@ 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, 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 +475,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 b30f4b1..b40a44c 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,15 @@ class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin): ] +class ResubmissionAdmin(BaseResourceAdmin): + list_display = [ + "resubmit_on" + ] + fields = [ + "comment", + "resubmit_on" + ] + # Outcommented for a cleaner admin backend on production #admin.site.register(Geometry, GeometryAdmin) @@ -148,3 +157,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 index 6701426..5d8f38e 100644 --- a/konova/forms.py +++ b/konova/forms.py @@ -13,7 +13,9 @@ from bootstrap_modal_forms.utils import is_ajax from django import forms from django.contrib import messages from django.contrib.gis import gdal +from django.core.exceptions import ObjectDoesNotExist from django.db.models.fields.files import FieldFile +from django.utils.timezone import now from compensation.models import EcoAccount from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP @@ -26,7 +28,7 @@ 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.models import BaseObject, Geometry, RecordableObjectMixin, AbstractDocument, Resubmission from konova.settings import DEFAULT_SRID from konova.tasks import celery_update_parcels from konova.utils.message_templates import FORM_INVALID, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE, DOCUMENT_EDITED @@ -161,6 +163,7 @@ class BaseForm(forms.Form): self, ( NewDeductionModalForm, + ResubmissionModalForm, EditEcoAccountDeductionModalForm, RemoveEcoAccountDeductionModalForm, ) @@ -686,3 +689,72 @@ class RecordModalForm(BaseModalForm): """ pass + + +class ResubmissionModalForm(BaseModalForm): + date = forms.DateField( + label_suffix=_(""), + label=_("Date"), + help_text=_("When do you want to be reminded?"), + widget=forms.DateInput( + attrs={ + "type": "date", + "data-provide": "datepicker", + "class": "form-control", + }, + format="%d.%m.%Y" + ) + ) + comment = forms.CharField( + required=False, + label=_("Comment"), + label_suffix=_(""), + help_text=_("Additional comment"), + widget=forms.Textarea( + attrs={ + "cols": 30, + "rows": 5, + "class": "form-control", + } + ) + ) + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.form_title = _("Resubmission") + self.form_caption = _("Set your resubmission for this entry.") + self.action_url = None + + try: + self.resubmission = self.instance.resubmissions.get( + user=self.user + ) + self.initialize_form_field("date", str(self.resubmission.resubmit_on)) + self.initialize_form_field("comment", self.resubmission.comment) + except ObjectDoesNotExist: + self.resubmission = Resubmission() + + def is_valid(self): + super_valid = super().is_valid() + self_valid = True + + date = self.cleaned_data.get("date") + today = now().today().date() + if date <= today: + self.add_error( + "date", + _("The date should be in the future") + ) + self_valid = False + + return super_valid and self_valid + + def save(self): + with transaction.atomic(): + self.resubmission.user = self.user + self.resubmission.resubmit_on = self.cleaned_data.get("date") + self.resubmission.comment = self.cleaned_data.get("comment") + self.resubmission.save() + self.instance.resubmissions.add(self.resubmission) + return self.resubmission + From 8a446818032ac489eac6acf5bf0f19653502560a Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 15 Aug 2022 10:02:07 +0200 Subject: [PATCH 3/4] Command * adds new command to be used with cron for periodic checkin of resubmissions * updates translations --- konova/admin.py | 3 +- .../commands/handle_resubmissions.py | 46 +++ konova/models/resubmission.py | 2 +- locale/de/LC_MESSAGES/django.mo | Bin 44517 -> 45149 bytes locale/de/LC_MESSAGES/django.po | 302 ++++++++++-------- 5 files changed, 226 insertions(+), 127 deletions(-) create mode 100644 konova/management/commands/handle_resubmissions.py diff --git a/konova/admin.py b/konova/admin.py index b40a44c..07d692d 100644 --- a/konova/admin.py +++ b/konova/admin.py @@ -145,7 +145,8 @@ class ResubmissionAdmin(BaseResourceAdmin): ] fields = [ "comment", - "resubmit_on" + "resubmit_on", + "resubmission_sent", ] diff --git a/konova/management/commands/handle_resubmissions.py b/konova/management/commands/handle_resubmissions.py new file mode 100644 index 0000000..047dbd5 --- /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/models/resubmission.py b/konova/models/resubmission.py index ca97ebb..be5fe84 100644 --- a/konova/models/resubmission.py +++ b/konova/models/resubmission.py @@ -35,7 +35,7 @@ class Resubmission(BaseResource): """ Sends a resubmission mail """ - _today = today() + _today = today().date() resubmission_handled = _today.__ge__(self.resubmit_on) and self.resubmission_sent if resubmission_handled: return diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index feda2678e509fe8e667237f2618882f674d2581e..03853f485e18ce219bb50fbd9b452d147282bbb3 100644 GIT binary patch delta 12882 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\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 " From 0bf2051bdf6debb3c508ee06f03c51970d104d2b Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 15 Aug 2022 10:50:01 +0200 Subject: [PATCH 4/4] Migrations + Cleanup * adds needed migrations * refactors forms.py (700+ lines) in main konova app * splits into forms/ and forms/modals and single class/topic-files for better maintainability and overview * fixes bug in main konova app migration which could occur if a certain compensation migration did not run before --- compensation/forms/modalForms.py | 2 +- .../migrations/0008_auto_20220815_0803.py | 24 + .../migrations/0009_auto_20220815_0803.py | 32 + .../migrations/0010_auto_20220815_1030.py | 24 + compensation/views/compensation.py | 3 +- compensation/views/eco_account.py | 5 +- compensation/views/payment.py | 1 - ema/forms.py | 5 +- ema/migrations/0005_ema_resubmission.py | 19 + ema/migrations/0006_auto_20220815_0803.py | 23 + ema/migrations/0007_auto_20220815_1030.py | 19 + ema/views.py | 3 +- intervention/forms/forms.py | 3 +- intervention/forms/modalForms.py | 3 +- .../0005_intervention_resubmission.py | 19 + .../migrations/0006_auto_20220815_0803.py | 23 + .../migrations/0007_auto_20220815_1030.py | 19 + intervention/views.py | 3 +- konova/forms.py | 760 ------------------ konova/forms/__init__.py | 11 + konova/forms/base_form.py | 157 ++++ konova/forms/geometry_form.py | 133 +++ konova/forms/modals/__init__.py | 12 + konova/forms/modals/base_form.py | 73 ++ konova/forms/modals/document_form.py | 163 ++++ konova/forms/modals/record_form.py | 123 +++ konova/forms/modals/remove_form.py | 58 ++ konova/forms/modals/resubmission_form.py | 85 ++ konova/forms/remove_form.py | 54 ++ konova/migrations/0005_auto_20220216_0856.py | 1 + konova/migrations/0014_resubmission.py | 33 + konova/models/object.py | 1 - konova/utils/documents.py | 5 +- user/forms.py | 3 +- user/migrations/0006_auto_20220815_0759.py | 18 + 35 files changed, 1143 insertions(+), 777 deletions(-) create mode 100644 compensation/migrations/0008_auto_20220815_0803.py create mode 100644 compensation/migrations/0009_auto_20220815_0803.py create mode 100644 compensation/migrations/0010_auto_20220815_1030.py create mode 100644 ema/migrations/0005_ema_resubmission.py create mode 100644 ema/migrations/0006_auto_20220815_0803.py create mode 100644 ema/migrations/0007_auto_20220815_1030.py create mode 100644 intervention/migrations/0005_intervention_resubmission.py create mode 100644 intervention/migrations/0006_auto_20220815_0803.py create mode 100644 intervention/migrations/0007_auto_20220815_1030.py delete mode 100644 konova/forms.py create mode 100644 konova/forms/__init__.py create mode 100644 konova/forms/base_form.py create mode 100644 konova/forms/geometry_form.py create mode 100644 konova/forms/modals/__init__.py create mode 100644 konova/forms/modals/base_form.py create mode 100644 konova/forms/modals/document_form.py create mode 100644 konova/forms/modals/record_form.py create mode 100644 konova/forms/modals/remove_form.py create mode 100644 konova/forms/modals/resubmission_form.py create mode 100644 konova/forms/remove_form.py create mode 100644 konova/migrations/0014_resubmission.py create mode 100644 user/migrations/0006_auto_20220815_0759.py diff --git a/compensation/forms/modalForms.py b/compensation/forms/modalForms.py index 581a7b3..ef21aa3 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 0000000..a4d6313 --- /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 0000000..a7c00e6 --- /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 0000000..2d3f16e --- /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/views/compensation.py b/compensation/views/compensation.py index 016f8ea..db01a04 100644 --- a/compensation/views/compensation.py +++ b/compensation/views/compensation.py @@ -14,8 +14,9 @@ from compensation.tables import CompensationTable from intervention.models import Intervention from konova.contexts import BaseContext from konova.decorators import * -from konova.forms import RemoveModalForm, SimpleGeomForm, RemoveDeadlineModalForm, EditDocumentModalForm, \ +from konova.forms.modals import RemoveModalForm,RemoveDeadlineModalForm, EditDocumentModalForm, \ ResubmissionModalForm +from konova.forms import SimpleGeomForm from konova.models import Deadline from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.documents import get_document, remove_document diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index 03109a8..2ebeb1f 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, \ +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, \ diff --git a/compensation/views/payment.py b/compensation/views/payment.py index 2be5455..84fad5b 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 a7e82c4..93f2349 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 0000000..57a1fbe --- /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 0000000..44ae765 --- /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 0000000..8442917 --- /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/views.py b/ema/views.py index 9cd6dd9..f07187a 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, \ +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 diff --git a/intervention/forms/forms.py b/intervention/forms/forms.py index b85ba10..15b02fd 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 b6445a5..a977c1c 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 0000000..ac48923 --- /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 0000000..8d0bf80 --- /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 0000000..b7a2729 --- /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/views.py b/intervention/views.py index c55fe72..6a9304e 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, ResubmissionModalForm +from konova.forms import SimpleGeomForm +from konova.forms.modals import RemoveModalForm, RecordModalForm, EditDocumentModalForm, ResubmissionModalForm from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.utils.documents import remove_document, get_document from konova.utils.generators import generate_qr_code diff --git a/konova/forms.py b/konova/forms.py deleted file mode 100644 index 5d8f38e..0000000 --- a/konova/forms.py +++ /dev/null @@ -1,760 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 16.11.20 - -""" -import json -from abc import abstractmethod - -from bootstrap_modal_forms.forms import BSModalForm -from bootstrap_modal_forms.utils import is_ajax -from django import forms -from django.contrib import messages -from django.contrib.gis import gdal -from django.core.exceptions import ObjectDoesNotExist -from django.db.models.fields.files import FieldFile -from django.utils.timezone import now - -from compensation.models import EcoAccount -from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP -from user.models import User -from django.contrib.gis.forms import MultiPolygonField -from django.contrib.gis.geos import MultiPolygon, Polygon -from django.db import transaction -from django.http import HttpRequest, HttpResponseRedirect -from django.shortcuts import render -from django.utils.translation import gettext_lazy as _ - -from konova.contexts import BaseContext -from konova.models import BaseObject, Geometry, RecordableObjectMixin, AbstractDocument, Resubmission -from konova.settings import DEFAULT_SRID -from konova.tasks import celery_update_parcels -from konova.utils.message_templates import FORM_INVALID, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE, DOCUMENT_EDITED -from user.models import UserActionLogEntry - - -class BaseForm(forms.Form): - """ - Basic form for that holds attributes needed in all other forms - """ - template = None - action_url = None - action_btn_label = _("Save") - form_title = None - cancel_redirect = None - form_caption = None - instance = None # The data holding model object - request = None - form_attrs = {} # Holds additional attributes, that can be used in the template - has_required_fields = False # Automatically set. Triggers hint rendering in templates - show_cancel_btn = True - - def __init__(self, *args, **kwargs): - self.instance = kwargs.pop("instance", None) - super().__init__(*args, **kwargs) - if self.request is not None: - self.user = self.request.user - # Check for required fields - for _field_name, _field_val in self.fields.items(): - if _field_val.required: - self.has_required_fields = True - break - - self.check_for_recorded_instance() - - @abstractmethod - def save(self): - # To be implemented in subclasses! - pass - - def disable_form_field(self, field: str): - """ - Disables a form field for user editing - """ - self.fields[field].widget.attrs["readonly"] = True - self.fields[field].disabled = True - self.fields[field].widget.attrs["title"] = _("Not editable") - - def initialize_form_field(self, field: str, val): - """ - Initializes a form field with a value - """ - self.fields[field].initial = val - - def add_placeholder_for_field(self, field: str, val): - """ - Adds a placeholder to a field after initialization without the need to redefine the form widget - - Args: - field (str): Field name - val (str): Placeholder - - Returns: - - """ - self.fields[field].widget.attrs["placeholder"] = val - - def load_initial_data(self, form_data: dict, disabled_fields: list = None): - """ Initializes form data from instance - - Inserts instance data into form and disables form fields - - Returns: - - """ - if self.instance is None: - return - for k, v in form_data.items(): - self.initialize_form_field(k, v) - if disabled_fields: - for field in disabled_fields: - self.disable_form_field(field) - - def add_widget_html_class(self, field: str, cls: str): - """ Adds a HTML class string to the widget of a field - - Args: - field (str): The field's name - cls (str): The new class string - - Returns: - - """ - set_class = self.fields[field].widget.attrs.get("class", "") - if cls in set_class: - return - else: - set_class += " " + cls - self.fields[field].widget.attrs["class"] = set_class - - def remove_widget_html_class(self, field: str, cls: str): - """ Removes a HTML class string from the widget of a field - - Args: - field (str): The field's name - cls (str): The new class string - - Returns: - - """ - set_class = self.fields[field].widget.attrs.get("class", "") - set_class = set_class.replace(cls, "") - self.fields[field].widget.attrs["class"] = set_class - - def check_for_recorded_instance(self): - """ Checks if the instance is recorded and runs some special logic if yes - - If the instance is recorded, the form shall not display any possibility to - edit any data. Instead, the users should get some information about why they can not edit anything. - - There are situations where the form should be rendered regularly, - e.g deduction forms for (recorded) eco accounts. - - Returns: - - """ - from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \ - RemoveEcoAccountDeductionModalForm - is_none = self.instance is None - is_other_data_type = not isinstance(self.instance, BaseObject) - is_deduction_form_from_account = isinstance( - self, - ( - NewDeductionModalForm, - ResubmissionModalForm, - EditEcoAccountDeductionModalForm, - RemoveEcoAccountDeductionModalForm, - ) - ) and isinstance(self.instance, EcoAccount) - - if is_none or is_other_data_type or is_deduction_form_from_account: - # Do nothing - return - - if self.instance.is_recorded: - self.template = "form/recorded_no_edit.html" - - -class RemoveForm(BaseForm): - check = forms.BooleanField( - label=_("Confirm"), - label_suffix=_(""), - required=True, - ) - - def __init__(self, *args, **kwargs): - self.object_to_remove = kwargs.pop("object_to_remove", None) - self.remove_post_url = kwargs.pop("remove_post_url", "") - self.cancel_url = kwargs.pop("cancel_url", "") - - super().__init__(*args, **kwargs) - - self.form_title = _("Remove") - if self.object_to_remove is not None: - self.form_caption = _("You are about to remove {} {}").format(self.object_to_remove.__class__.__name__, self.object_to_remove) - self.action_url = self.remove_post_url - self.cancel_redirect = self.cancel_url - - def is_checked(self) -> bool: - return self.cleaned_data.get("check", False) - - def save(self, user: User): - """ Perform generic removing by running the form typical 'save()' method - - Args: - user (User): The performing user - - Returns: - - """ - if self.object_to_remove is not None and self.is_checked(): - with transaction.atomic(): - self.object_to_remove.is_active = False - action = UserActionLogEntry.get_deleted_action(user) - self.object_to_remove.deleted = action - self.object_to_remove.save() - return self.object_to_remove - - -class BaseModalForm(BaseForm, BSModalForm): - """ A specialzed form class for modal form handling - - """ - is_modal_form = True - render_submit = True - template = "modal/modal_form.html" - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.action_btn_label = _("Continue") - - def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None): - """ Generic processing of request - - Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used - - Args: - request (HttpRequest): The incoming request - msg_success (str): The message in case of successful removing - msg_error (str): The message in case of an error - - Returns: - - """ - redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home") - template = self.template - if request.method == "POST": - if self.is_valid(): - if not is_ajax(request.META): - # Modal forms send one POST for checking on data validity. This can be used to return possible errors - # on the form. A second POST (if no errors occured) is sent afterwards and needs to process the - # saving/commiting of the data to the database. is_ajax() performs this check. The first request is - # an ajax call, the second is a regular form POST. - self.save() - messages.success( - request, - msg_success - ) - return HttpResponseRedirect(redirect_url) - else: - context = { - "form": self, - } - context = BaseContext(request, context).context - return render(request, template, context) - elif request.method == "GET": - context = { - "form": self, - } - context = BaseContext(request, context).context - return render(request, template, context) - else: - raise NotImplementedError - - -class SimpleGeomForm(BaseForm): - """ A geometry form for rendering geometry read-only using a widget - - """ - read_only = True - geom = MultiPolygonField( - srid=DEFAULT_SRID_RLP, - label=_("Geometry"), - help_text=_(""), - label_suffix="", - required=False, - disabled=False, - ) - - def __init__(self, *args, **kwargs): - self.read_only = kwargs.pop("read_only", True) - super().__init__(*args, **kwargs) - - # Initialize geometry - try: - geom = self.instance.geometry.geom - self.empty = geom.empty - - if self.empty: - raise AttributeError - - geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP) - geom = json.dumps(geojson) - except AttributeError: - # If no geometry exists for this form, we simply set the value to None and zoom to the maximum level - geom = "" - self.empty = True - - self.initialize_form_field("geom", geom) - - def is_valid(self): - super().is_valid() - is_valid = True - - # Get geojson from form - geom = self.data["geom"] - if geom is None or len(geom) == 0: - # empty geometry is a valid geometry - return is_valid - geom = json.loads(geom) - - # Write submitted data back into form field to make sure invalid geometry - # will be rendered again on failed submit - self.initialize_form_field("geom", self.data["geom"]) - - # Read geojson into gdal geometry - # HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for - # this case) - features = [] - features_json = geom.get("features", []) - for feature in features_json: - g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP) - if g.geom_type not in ["Polygon", "MultiPolygon"]: - self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered.")) - is_valid = False - return is_valid - - polygon = Polygon.from_ewkt(g.ewkt) - is_valid = polygon.valid - if not is_valid: - self.add_error("geom", polygon.valid_reason) - return is_valid - - features.append(polygon) - form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP) - for feature in features: - form_geom = form_geom.union(feature) - - # Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided. - if form_geom.geom_type != "MultiPolygon": - form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP) - - # Write unioned Multipolygon into cleaned data - if self.cleaned_data is None: - self.cleaned_data = {} - self.cleaned_data["geom"] = form_geom.ewkt - - return is_valid - - def save(self, action: UserActionLogEntry): - """ Saves the form's geometry - - Creates a new geometry entry if none is set, yet - - Args: - action (): - - Returns: - - """ - try: - if self.instance is None or self.instance.geometry is None: - raise LookupError - geometry = self.instance.geometry - geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)) - geometry.modified = action - - geometry.save() - except LookupError: - # No geometry or linked instance holding a geometry exist --> create a new one! - geometry = Geometry.objects.create( - geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)), - created=action, - ) - # Start the parcel update procedure in a background process - celery_update_parcels.delay(geometry.id) - return geometry - - -class RemoveModalForm(BaseModalForm): - """ Generic removing modal form - - Can be used for anything, where removing shall be confirmed by the user a second time. - - """ - confirm = forms.BooleanField( - label=_("Confirm"), - label_suffix=_(""), - widget=forms.CheckboxInput(), - required=True, - ) - - def __init__(self, *args, **kwargs): - self.template = "modal/modal_form.html" - super().__init__(*args, **kwargs) - self.form_title = _("Remove") - self.form_caption = _("Are you sure?") - # Disable automatic w-100 setting for this type of modal form. Looks kinda strange - self.fields["confirm"].widget.attrs["class"] = "" - - def save(self): - if isinstance(self.instance, BaseObject): - self.instance.mark_as_deleted(self.user) - else: - # If the class does not provide restorable delete functionality, we must delete the entry finally - self.instance.delete() - - -class RemoveDeadlineModalForm(RemoveModalForm): - """ Removing modal form for deadlines - - Can be used for anything, where removing shall be confirmed by the user a second time. - - """ - deadline = None - - def __init__(self, *args, **kwargs): - deadline = kwargs.pop("deadline", None) - self.deadline = deadline - super().__init__(*args, **kwargs) - - def save(self): - self.instance.remove_deadline(self) - - -class NewDocumentModalForm(BaseModalForm): - """ Modal form for new documents - - """ - title = forms.CharField( - label=_("Title"), - label_suffix=_(""), - max_length=500, - widget=forms.TextInput( - attrs={ - "class": "form-control", - } - ) - ) - creation_date = forms.DateField( - label=_("Created on"), - label_suffix=_(""), - help_text=_("When has this file been created? Important for photos."), - widget=forms.DateInput( - attrs={ - "type": "date", - "data-provide": "datepicker", - "class": "form-control", - }, - format="%d.%m.%Y" - ) - ) - file = forms.FileField( - label=_("File"), - label_suffix=_(""), - help_text=_("Allowed formats: pdf, jpg, png. Max size 15 MB."), - widget=forms.FileInput( - attrs={ - "class": "form-control-file", - } - ), - ) - comment = forms.CharField( - required=False, - max_length=200, - label=_("Comment"), - label_suffix=_(""), - help_text=_("Additional comment, maximum {} letters").format(200), - widget=forms.Textarea( - attrs={ - "cols": 30, - "rows": 5, - "class": "form-control", - } - ) - ) - document_model = None - - class Meta: - abstract = True - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Add new document") - self.form_caption = _("") - self.form_attrs = { - "enctype": "multipart/form-data", # important for file upload - } - if not self.document_model: - raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__)) - - def is_valid(self): - super_valid = super().is_valid() - - _file = self.cleaned_data.get("file", None) - - if _file is None or isinstance(_file, FieldFile): - # FieldFile declares that no new file has been uploaded and we do not need to check on the file again - return super_valid - - mime_type_valid = self.document_model.is_mime_type_valid(_file) - if not mime_type_valid: - self.add_error( - "file", - FILE_TYPE_UNSUPPORTED - ) - - file_size_valid = self.document_model.is_file_size_valid(_file) - if not file_size_valid: - self.add_error( - "file", - FILE_SIZE_TOO_LARGE - ) - - file_valid = mime_type_valid and file_size_valid - return super_valid and file_valid - - def save(self): - with transaction.atomic(): - action = UserActionLogEntry.get_created_action(self.user) - edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document")) - - doc = self.document_model.objects.create( - created=action, - title=self.cleaned_data["title"], - comment=self.cleaned_data["comment"], - file=self.cleaned_data["file"], - date_of_creation=self.cleaned_data["creation_date"], - instance=self.instance, - ) - - self.instance.log.add(edited_action) - self.instance.modified = edited_action - self.instance.save() - - return doc - - -class EditDocumentModalForm(NewDocumentModalForm): - document = None - document_model = AbstractDocument - - def __init__(self, *args, **kwargs): - self.document = kwargs.pop("document", None) - super().__init__(*args, **kwargs) - self.form_title = _("Edit document") - form_data = { - "title": self.document.title, - "comment": self.document.comment, - "creation_date": str(self.document.date_of_creation), - "file": self.document.file, - } - self.load_initial_data(form_data) - - - def save(self): - with transaction.atomic(): - document = self.document - file = self.cleaned_data.get("file", None) - - document.title = self.cleaned_data.get("title", None) - document.comment = self.cleaned_data.get("comment", None) - document.date_of_creation = self.cleaned_data.get("creation_date", None) - if not isinstance(file, FieldFile): - document.replace_file(file) - document.save() - - self.instance.mark_as_edited(self.user, self.request, edit_comment=DOCUMENT_EDITED) - - return document - - -class RecordModalForm(BaseModalForm): - """ Modal form for recording data - - """ - confirm = forms.BooleanField( - label=_("Confirm record"), - label_suffix="", - widget=forms.CheckboxInput(), - required=True, - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Record data") - self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) - # Disable automatic w-100 setting for this type of modal form. Looks kinda strange - self.fields["confirm"].widget.attrs["class"] = "" - - if self.instance.recorded: - # unrecord! - self.fields["confirm"].label = _("Confirm unrecord") - self.form_title = _("Unrecord data") - self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name) - - if not isinstance(self.instance, RecordableObjectMixin): - raise NotImplementedError - - def is_valid(self): - """ Checks for instance's validity and data quality - - Returns: - - """ - from intervention.models import Intervention - super_val = super().is_valid() - if self.instance.recorded: - # If user wants to unrecord an already recorded dataset, we do not need to perform custom checks - return super_val - checker = self.instance.quality_check() - for msg in checker.messages: - self.add_error( - "confirm", - msg - ) - valid = checker.valid - # Special case: Intervention - # Add direct checks for related compensations - if isinstance(self.instance, Intervention): - comps_valid = self._are_compensations_valid() - valid = valid and comps_valid - return super_val and valid - - def _are_deductions_valid(self): - """ Performs validity checks on deductions and their eco-account - - Returns: - - """ - deductions = self.instance.deductions.all() - for deduction in deductions: - checker = deduction.account.quality_check() - for msg in checker.messages: - self.add_error( - "confirm", - f"{deduction.account.identifier}: {msg}" - ) - return checker.valid - return True - - def _are_compensations_valid(self): - """ Runs a special case for intervention-compensations validity - - Returns: - - """ - comps = self.instance.compensations.filter( - deleted=None, - ) - comps_valid = True - for comp in comps: - checker = comp.quality_check() - comps_valid = comps_valid and checker.valid - for msg in checker.messages: - self.add_error( - "confirm", - f"{comp.identifier}: {msg}" - ) - - deductions_valid = self._are_deductions_valid() - - return comps_valid and deductions_valid - - def save(self): - with transaction.atomic(): - if self.cleaned_data["confirm"]: - if self.instance.recorded: - self.instance.set_unrecorded(self.user) - else: - self.instance.set_recorded(self.user) - return self.instance - - def check_for_recorded_instance(self): - """ Overwrite the check method for doing nothing on the RecordModalForm - - Returns: - - """ - pass - - -class ResubmissionModalForm(BaseModalForm): - date = forms.DateField( - label_suffix=_(""), - label=_("Date"), - help_text=_("When do you want to be reminded?"), - widget=forms.DateInput( - attrs={ - "type": "date", - "data-provide": "datepicker", - "class": "form-control", - }, - format="%d.%m.%Y" - ) - ) - comment = forms.CharField( - required=False, - label=_("Comment"), - label_suffix=_(""), - help_text=_("Additional comment"), - widget=forms.Textarea( - attrs={ - "cols": 30, - "rows": 5, - "class": "form-control", - } - ) - ) - - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - self.form_title = _("Resubmission") - self.form_caption = _("Set your resubmission for this entry.") - self.action_url = None - - try: - self.resubmission = self.instance.resubmissions.get( - user=self.user - ) - self.initialize_form_field("date", str(self.resubmission.resubmit_on)) - self.initialize_form_field("comment", self.resubmission.comment) - except ObjectDoesNotExist: - self.resubmission = Resubmission() - - def is_valid(self): - super_valid = super().is_valid() - self_valid = True - - date = self.cleaned_data.get("date") - today = now().today().date() - if date <= today: - self.add_error( - "date", - _("The date should be in the future") - ) - self_valid = False - - return super_valid and self_valid - - def save(self): - with transaction.atomic(): - self.resubmission.user = self.user - self.resubmission.resubmit_on = self.cleaned_data.get("date") - self.resubmission.comment = self.cleaned_data.get("comment") - self.resubmission.save() - self.instance.resubmissions.add(self.resubmission) - return self.resubmission - diff --git a/konova/forms/__init__.py b/konova/forms/__init__.py new file mode 100644 index 0000000..5840c4d --- /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 0000000..065fba1 --- /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 0000000..3c957aa --- /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 0000000..f922f2d --- /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 0000000..a680657 --- /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 0000000..96b4f8e --- /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 0000000..812b697 --- /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 0000000..7a14626 --- /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 0000000..d1d846f --- /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 0000000..d5c884a --- /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/migrations/0005_auto_20220216_0856.py b/konova/migrations/0005_auto_20220216_0856.py index 567e206..8626b7c 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 0000000..f0ef9e7 --- /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/object.py b/konova/models/object.py index 0fbd6e8..8af95e1 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -749,7 +749,6 @@ class GeoReferencedMixin(models.Model): class ResubmitableObjectMixin(models.Model): resubmissions = models.ManyToManyField( "konova.Resubmission", - null=True, blank=True, related_name="+", ) diff --git a/konova/utils/documents.py b/konova/utils/documents.py index f9b1516..3e8f6f1 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/user/forms.py b/user/forms.py index 12688b4..0649eaf 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 0000000..5386108 --- /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), + ), + ]