Merge pull request '197_Resubmission' (#198) from 197_Resubmission into master

Reviewed-on: SGD-Nord/konova#198
pull/199/head v0.6
mpeltriaux 2 years ago
commit 180ee293ca

@ -20,7 +20,7 @@ from compensation.models import CompensationDocument, EcoAccountDocument
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \ from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \
CompensationStateTreeRadioSelect CompensationStateTreeRadioSelect
from konova.contexts import BaseContext 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.models import DeadlineType
from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, \ from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, \
ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED

@ -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'),
),
]

@ -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'),
),
]

@ -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'),
),
]

@ -19,14 +19,17 @@ from compensation.managers import CompensationManager
from compensation.models import CompensationState, CompensationAction from compensation.models import CompensationState, CompensationAction
from compensation.utils.quality import CompensationQualityChecker from compensation.utils.quality import CompensationQualityChecker
from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \ 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, \ from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \
DOCUMENT_REMOVED_TEMPLATE, DEADLINE_REMOVED, ADDED_DEADLINE, \ DOCUMENT_REMOVED_TEMPLATE, DEADLINE_REMOVED, ADDED_DEADLINE, \
COMPENSATION_ACTION_REMOVED, COMPENSATION_STATE_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE COMPENSATION_ACTION_REMOVED, COMPENSATION_STATE_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
from user.models import UserActionLogEntry 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, Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation,
EMA or EcoAccount. EMA or EcoAccount.

@ -12,6 +12,9 @@
</button> </button>
</a> </a>
{% if has_access %} {% if has_access %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:resubmission-create' obj.id %}">
{% fa5_icon 'bell' %}
</button>
{% if is_default_member %} {% if is_default_member %}
<a href="{% url 'compensation:edit' obj.id %}" class="mr-2"> <a href="{% url 'compensation:edit' obj.id %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}"> <button class="btn btn-default" title="{% trans 'Edit' %}">

@ -12,6 +12,9 @@
</button> </button>
</a> </a>
{% if has_access %} {% if has_access %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:acc:resubmission-create' obj.id %}">
{% fa5_icon 'bell' %}
</button>
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'compensation:acc:share-create' obj.id %}"> <button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'compensation:acc:share-create' obj.id %}">
{% fa5_icon 'share-alt' %} {% fa5_icon 'share-alt' %}
</button> </button>

@ -31,6 +31,7 @@ urlpatterns = [
path('<id>/deadline/<deadline_id>/edit', deadline_edit_view, name='deadline-edit'), path('<id>/deadline/<deadline_id>/edit', deadline_edit_view, name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'), path('<id>/deadline/<deadline_id>/remove', deadline_remove_view, name='deadline-remove'),
path('<id>/report', report_view, name='report'), path('<id>/report', report_view, name='report'),
path('<id>/resub', create_resubmission_view, name='resubmission-create'),
# Documents # Documents
path('<id>/document/new/', new_document_view, name='new-doc'), path('<id>/document/new/', new_document_view, name='new-doc'),

@ -19,6 +19,7 @@ urlpatterns = [
path('<id>/report', report_view, name='report'), path('<id>/report', report_view, name='report'),
path('<id>/edit', edit_view, name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'), path('<id>/remove', remove_view, name='remove'),
path('<id>/resub', create_resubmission_view, name='resubmission-create'),
path('<id>/state/new', state_new_view, name='new-state'), path('<id>/state/new', state_new_view, name='new-state'),
path('<id>/state/<state_id>/edit', state_edit_view, name='state-edit'), path('<id>/state/<state_id>/edit', state_edit_view, name='state-edit'),

@ -14,7 +14,9 @@ from compensation.tables import CompensationTable
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import * 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.models import Deadline
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.documents import get_document, remove_document from konova.utils.documents import get_document, remove_document
@ -656,3 +658,26 @@ def report_view(request: HttpRequest, id: str):
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, 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,))
)

@ -25,14 +25,15 @@ from intervention.forms.modalForms import NewDeductionModalForm, ShareModalForm,
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \ from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \
shared_access_required shared_access_required
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentModalForm, RecordModalForm, \ from konova.forms.modals import RemoveModalForm, RecordModalForm, \
RemoveDeadlineModalForm, EditDocumentModalForm RemoveDeadlineModalForm, EditDocumentModalForm, ResubmissionModalForm
from konova.forms import SimpleGeomForm
from konova.models import Deadline from konova.models import Deadline
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.documents import get_document, remove_document from konova.utils.documents import get_document, remove_document
from konova.utils.generators import generate_qr_code 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, \ 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, \ 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, \ DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, \
@ -839,3 +840,26 @@ def create_share_view(request: HttpRequest, id: str):
request, request,
msg_success=_("Share settings updated") 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,))
)

@ -15,7 +15,6 @@ from compensation.forms.modalForms import NewPaymentForm, RemovePaymentModalForm
from compensation.models import Payment from compensation.models import Payment
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import default_group_required, shared_access_required 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 from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED

@ -5,8 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 06.10.21 Created on: 06.10.21
""" """
from dal import autocomplete
from django import forms
from user.models import User from user.models import User
from django.db import transaction from django.db import transaction
from django.urls import reverse, reverse_lazy from django.urls import reverse, reverse_lazy
@ -16,7 +14,8 @@ from compensation.forms.forms import AbstractCompensationForm, CompensationRespo
PikCompensationFormMixin PikCompensationFormMixin
from ema.models import Ema, EmaDocument from ema.models import Ema, EmaDocument
from intervention.models import Responsibility, Handler 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 from user.models import UserActionLogEntry

@ -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'),
),
]

@ -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'),
),
]

@ -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'),
),
]

@ -12,6 +12,9 @@
</button> </button>
</a> </a>
{% if has_access %} {% if has_access %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}">
{% fa5_icon 'bell' %}
</button>
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-create' obj.id %}"> <button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-create' obj.id %}">
{% fa5_icon 'share-alt' %} {% fa5_icon 'share-alt' %}
</button> </button>

@ -19,6 +19,7 @@ urlpatterns = [
path('<id>/remove', remove_view, name='remove'), path('<id>/remove', remove_view, name='remove'),
path('<id>/record', record_view, name='record'), path('<id>/record', record_view, name='record'),
path('<id>/report', report_view, name='report'), path('<id>/report', report_view, name='report'),
path('<id>/resub', create_resubmission_view, name='resubmission-create'),
path('<id>/state/new', state_new_view, name='new-state'), path('<id>/state/new', state_new_view, name='new-state'),
path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'), path('<id>/state/<state_id>/remove', state_remove_view, name='state-remove'),

@ -16,8 +16,9 @@ from intervention.forms.modalForms import ShareModalForm
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import conservation_office_group_required, shared_access_required from konova.decorators import conservation_office_group_required, shared_access_required
from ema.models import Ema, EmaDocument from ema.models import Ema, EmaDocument
from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm, RemoveDeadlineModalForm, \ from konova.forms.modals import RemoveModalForm, RecordModalForm, RemoveDeadlineModalForm, \
EditDocumentModalForm EditDocumentModalForm, ResubmissionModalForm
from konova.forms import SimpleGeomForm
from konova.models import Deadline from konova.models import Deadline
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
@ -711,3 +712,26 @@ def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
msg_success=DEADLINE_REMOVED, msg_success=DEADLINE_REMOVED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data" redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
) )
@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,))
)

@ -8,6 +8,7 @@ Created on: 02.12.20
from dal import autocomplete from dal import autocomplete
from django import forms from django import forms
from konova.forms.base_form import BaseForm
from konova.utils.message_templates import EDITED_GENERAL_DATA from konova.utils.message_templates import EDITED_GENERAL_DATA
from user.models import User from user.models import User
from django.db import transaction 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 CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
from intervention.inputs import GenerateInput from intervention.inputs import GenerateInput
from intervention.models import Intervention, Legal, Responsibility, Handler 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 from user.models import UserActionLogEntry

@ -19,7 +19,8 @@ from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount, EcoAccountDeduction from compensation.models import EcoAccount, EcoAccountDeduction
from intervention.inputs import TextToClipboardInput from intervention.inputs import TextToClipboardInput
from intervention.models import Intervention, InterventionDocument, RevocationDocument 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.general import format_german_float
from konova.utils.user_checks import is_default_group_only from konova.utils.user_checks import is_default_group_only

@ -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'),
),
]

@ -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'),
),
]

@ -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'),
),
]

@ -26,14 +26,19 @@ from intervention.models.revocation import RevocationDocument, Revocation
from intervention.utils.quality import InterventionQualityChecker from intervention.utils.quality import InterventionQualityChecker
from konova.models import generate_document_file_upload_path, AbstractDocument, BaseObject, \ from konova.models import generate_document_file_upload_path, AbstractDocument, BaseObject, \
ShareableObjectMixin, \ ShareableObjectMixin, \
RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin, ResubmitableObjectMixin
from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE, \ from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE, \
PAYMENT_REMOVED, PAYMENT_ADDED, REVOCATION_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE PAYMENT_REMOVED, PAYMENT_ADDED, REVOCATION_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
from user.models import UserActionLogEntry 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. Interventions are e.g. construction sites where nature used to be.
""" """

@ -12,6 +12,9 @@
</button> </button>
</a> </a>
{% if has_access %} {% if has_access %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'intervention:resubmission-create' obj.id %}">
{% fa5_icon 'bell' %}
</button>
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'intervention:share-create' obj.id %}"> <button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'intervention:share-create' obj.id %}">
{% fa5_icon 'share-alt' %} {% fa5_icon 'share-alt' %}
</button> </button>

@ -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, \ 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, \ 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, \ 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" app_name = "intervention"
urlpatterns = [ urlpatterns = [
@ -26,6 +27,7 @@ urlpatterns = [
path('<id>/check', check_view, name='check'), path('<id>/check', check_view, name='check'),
path('<id>/record', record_view, name='record'), path('<id>/record', record_view, name='record'),
path('<id>/report', report_view, name='report'), path('<id>/report', report_view, name='report'),
path('<id>/resub', create_resubmission_view, name='resubmission-create'),
# Compensations # Compensations
path('<id>/compensation/<comp_id>/remove', remove_compensation_view, name='remove-compensation'), path('<id>/compensation/<comp_id>/remove', remove_compensation_view, name='remove-compensation'),

@ -12,7 +12,8 @@ from intervention.models import Intervention, Revocation, InterventionDocument,
from intervention.tables import InterventionTable from intervention.tables import InterventionTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import * from konova.decorators import *
from konova.forms import SimpleGeomForm, RemoveModalForm, RecordModalForm, EditDocumentModalForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm, RecordModalForm, EditDocumentModalForm, ResubmissionModalForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.documents import remove_document, get_document from konova.utils.documents import remove_document, get_document
from konova.utils.generators import generate_qr_code from konova.utils.generators import generate_qr_code
@ -475,6 +476,29 @@ def create_share_view(request: HttpRequest, id: str):
) )
@login_required
@default_group_required
@shared_access_required(Intervention, "id")
def create_resubmission_view(request: HttpRequest, id: str):
""" Renders resubmission form for an intervention
Args:
request (HttpRequest): The incoming request
id (str): Intervention's id
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
form = ResubmissionModalForm(request.POST or None, instance=intervention, request=request)
form.action_url = reverse("intervention:resubmission-create", args=(id,))
return form.process_request(
request,
msg_success=_("Resubmission set"),
redirect_url=reverse("intervention:detail", args=(id,))
)
@login_required @login_required
@registration_office_group_required @registration_office_group_required
@shared_access_required(Intervention, "id") @shared_access_required(Intervention, "id")

@ -7,7 +7,7 @@ Created on: 22.07.21
""" """
from django.contrib import admin 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.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from user.models import UserAction from user.models import UserAction
@ -139,6 +139,16 @@ class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
] ]
class ResubmissionAdmin(BaseResourceAdmin):
list_display = [
"resubmit_on"
]
fields = [
"comment",
"resubmit_on",
"resubmission_sent",
]
# Outcommented for a cleaner admin backend on production # Outcommented for a cleaner admin backend on production
#admin.site.register(Geometry, GeometryAdmin) #admin.site.register(Geometry, GeometryAdmin)
@ -148,3 +158,4 @@ class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
#admin.site.register(ParcelGroup, ParcelGroupAdmin) #admin.site.register(ParcelGroup, ParcelGroupAdmin)
#admin.site.register(GeometryConflict, GeometryConflictAdmin) #admin.site.register(GeometryConflict, GeometryConflictAdmin)
#admin.site.register(Deadline, DeadlineAdmin) #admin.site.register(Deadline, DeadlineAdmin)
#admin.site.register(Resubmission, ResubmissionAdmin)

@ -1,688 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.11.20
"""
import json
from abc import abstractmethod
from bootstrap_modal_forms.forms import BSModalForm
from bootstrap_modal_forms.utils import is_ajax
from django import forms
from django.contrib import messages
from django.contrib.gis import gdal
from django.db.models.fields.files import FieldFile
from compensation.models import EcoAccount
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from user.models import User
from django.contrib.gis.forms import MultiPolygonField
from django.contrib.gis.geos import MultiPolygon, Polygon
from django.db import transaction
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext
from konova.models import BaseObject, Geometry, RecordableObjectMixin, AbstractDocument
from konova.settings import DEFAULT_SRID
from konova.tasks import celery_update_parcels
from konova.utils.message_templates import FORM_INVALID, FILE_TYPE_UNSUPPORTED, FILE_SIZE_TOO_LARGE, DOCUMENT_EDITED
from user.models import UserActionLogEntry
class BaseForm(forms.Form):
"""
Basic form for that holds attributes needed in all other forms
"""
template = None
action_url = None
action_btn_label = _("Save")
form_title = None
cancel_redirect = None
form_caption = None
instance = None # The data holding model object
request = None
form_attrs = {} # Holds additional attributes, that can be used in the template
has_required_fields = False # Automatically set. Triggers hint rendering in templates
show_cancel_btn = True
def __init__(self, *args, **kwargs):
self.instance = kwargs.pop("instance", None)
super().__init__(*args, **kwargs)
if self.request is not None:
self.user = self.request.user
# Check for required fields
for _field_name, _field_val in self.fields.items():
if _field_val.required:
self.has_required_fields = True
break
self.check_for_recorded_instance()
@abstractmethod
def save(self):
# To be implemented in subclasses!
pass
def disable_form_field(self, field: str):
"""
Disables a form field for user editing
"""
self.fields[field].widget.attrs["readonly"] = True
self.fields[field].disabled = True
self.fields[field].widget.attrs["title"] = _("Not editable")
def initialize_form_field(self, field: str, val):
"""
Initializes a form field with a value
"""
self.fields[field].initial = val
def add_placeholder_for_field(self, field: str, val):
"""
Adds a placeholder to a field after initialization without the need to redefine the form widget
Args:
field (str): Field name
val (str): Placeholder
Returns:
"""
self.fields[field].widget.attrs["placeholder"] = val
def load_initial_data(self, form_data: dict, disabled_fields: list = None):
""" Initializes form data from instance
Inserts instance data into form and disables form fields
Returns:
"""
if self.instance is None:
return
for k, v in form_data.items():
self.initialize_form_field(k, v)
if disabled_fields:
for field in disabled_fields:
self.disable_form_field(field)
def add_widget_html_class(self, field: str, cls: str):
""" Adds a HTML class string to the widget of a field
Args:
field (str): The field's name
cls (str): The new class string
Returns:
"""
set_class = self.fields[field].widget.attrs.get("class", "")
if cls in set_class:
return
else:
set_class += " " + cls
self.fields[field].widget.attrs["class"] = set_class
def remove_widget_html_class(self, field: str, cls: str):
""" Removes a HTML class string from the widget of a field
Args:
field (str): The field's name
cls (str): The new class string
Returns:
"""
set_class = self.fields[field].widget.attrs.get("class", "")
set_class = set_class.replace(cls, "")
self.fields[field].widget.attrs["class"] = set_class
def check_for_recorded_instance(self):
""" Checks if the instance is recorded and runs some special logic if yes
If the instance is recorded, the form shall not display any possibility to
edit any data. Instead, the users should get some information about why they can not edit anything.
There are situations where the form should be rendered regularly,
e.g deduction forms for (recorded) eco accounts.
Returns:
"""
from intervention.forms.modalForms import NewDeductionModalForm, EditEcoAccountDeductionModalForm, \
RemoveEcoAccountDeductionModalForm
is_none = self.instance is None
is_other_data_type = not isinstance(self.instance, BaseObject)
is_deduction_form_from_account = isinstance(
self,
(
NewDeductionModalForm,
EditEcoAccountDeductionModalForm,
RemoveEcoAccountDeductionModalForm,
)
) and isinstance(self.instance, EcoAccount)
if is_none or is_other_data_type or is_deduction_form_from_account:
# Do nothing
return
if self.instance.is_recorded:
self.template = "form/recorded_no_edit.html"
class RemoveForm(BaseForm):
check = forms.BooleanField(
label=_("Confirm"),
label_suffix=_(""),
required=True,
)
def __init__(self, *args, **kwargs):
self.object_to_remove = kwargs.pop("object_to_remove", None)
self.remove_post_url = kwargs.pop("remove_post_url", "")
self.cancel_url = kwargs.pop("cancel_url", "")
super().__init__(*args, **kwargs)
self.form_title = _("Remove")
if self.object_to_remove is not None:
self.form_caption = _("You are about to remove {} {}").format(self.object_to_remove.__class__.__name__, self.object_to_remove)
self.action_url = self.remove_post_url
self.cancel_redirect = self.cancel_url
def is_checked(self) -> bool:
return self.cleaned_data.get("check", False)
def save(self, user: User):
""" Perform generic removing by running the form typical 'save()' method
Args:
user (User): The performing user
Returns:
"""
if self.object_to_remove is not None and self.is_checked():
with transaction.atomic():
self.object_to_remove.is_active = False
action = UserActionLogEntry.get_deleted_action(user)
self.object_to_remove.deleted = action
self.object_to_remove.save()
return self.object_to_remove
class BaseModalForm(BaseForm, BSModalForm):
""" A specialzed form class for modal form handling
"""
is_modal_form = True
render_submit = True
template = "modal/modal_form.html"
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.action_btn_label = _("Continue")
def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
""" Generic processing of request
Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used
Args:
request (HttpRequest): The incoming request
msg_success (str): The message in case of successful removing
msg_error (str): The message in case of an error
Returns:
"""
redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home")
template = self.template
if request.method == "POST":
if self.is_valid():
if not is_ajax(request.META):
# Modal forms send one POST for checking on data validity. This can be used to return possible errors
# on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
# saving/commiting of the data to the database. is_ajax() performs this check. The first request is
# an ajax call, the second is a regular form POST.
self.save()
messages.success(
request,
msg_success
)
return HttpResponseRedirect(redirect_url)
else:
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
elif request.method == "GET":
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
else:
raise NotImplementedError
class SimpleGeomForm(BaseForm):
""" A geometry form for rendering geometry read-only using a widget
"""
read_only = True
geom = MultiPolygonField(
srid=DEFAULT_SRID_RLP,
label=_("Geometry"),
help_text=_(""),
label_suffix="",
required=False,
disabled=False,
)
def __init__(self, *args, **kwargs):
self.read_only = kwargs.pop("read_only", True)
super().__init__(*args, **kwargs)
# Initialize geometry
try:
geom = self.instance.geometry.geom
self.empty = geom.empty
if self.empty:
raise AttributeError
geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
geom = json.dumps(geojson)
except AttributeError:
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
geom = ""
self.empty = True
self.initialize_form_field("geom", geom)
def is_valid(self):
super().is_valid()
is_valid = True
# Get geojson from form
geom = self.data["geom"]
if geom is None or len(geom) == 0:
# empty geometry is a valid geometry
return is_valid
geom = json.loads(geom)
# Write submitted data back into form field to make sure invalid geometry
# will be rendered again on failed submit
self.initialize_form_field("geom", self.data["geom"])
# Read geojson into gdal geometry
# HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for
# this case)
features = []
features_json = geom.get("features", [])
for feature in features_json:
g = gdal.OGRGeometry(json.dumps(feature.get("geometry", feature)), srs=DEFAULT_SRID_RLP)
if g.geom_type not in ["Polygon", "MultiPolygon"]:
self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
is_valid = False
return is_valid
polygon = Polygon.from_ewkt(g.ewkt)
is_valid = polygon.valid
if not is_valid:
self.add_error("geom", polygon.valid_reason)
return is_valid
features.append(polygon)
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
for feature in features:
form_geom = form_geom.union(feature)
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
if form_geom.geom_type != "MultiPolygon":
form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP)
# Write unioned Multipolygon into cleaned data
if self.cleaned_data is None:
self.cleaned_data = {}
self.cleaned_data["geom"] = form_geom.ewkt
return is_valid
def save(self, action: UserActionLogEntry):
""" Saves the form's geometry
Creates a new geometry entry if none is set, yet
Args:
action ():
Returns:
"""
try:
if self.instance is None or self.instance.geometry is None:
raise LookupError
geometry = self.instance.geometry
geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP))
geometry.modified = action
geometry.save()
except LookupError:
# No geometry or linked instance holding a geometry exist --> create a new one!
geometry = Geometry.objects.create(
geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)),
created=action,
)
# Start the parcel update procedure in a background process
celery_update_parcels.delay(geometry.id)
return geometry
class RemoveModalForm(BaseModalForm):
""" Generic removing modal form
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
confirm = forms.BooleanField(
label=_("Confirm"),
label_suffix=_(""),
widget=forms.CheckboxInput(),
required=True,
)
def __init__(self, *args, **kwargs):
self.template = "modal/modal_form.html"
super().__init__(*args, **kwargs)
self.form_title = _("Remove")
self.form_caption = _("Are you sure?")
# Disable automatic w-100 setting for this type of modal form. Looks kinda strange
self.fields["confirm"].widget.attrs["class"] = ""
def save(self):
if isinstance(self.instance, BaseObject):
self.instance.mark_as_deleted(self.user)
else:
# If the class does not provide restorable delete functionality, we must delete the entry finally
self.instance.delete()
class RemoveDeadlineModalForm(RemoveModalForm):
""" Removing modal form for deadlines
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
deadline = None
def __init__(self, *args, **kwargs):
deadline = kwargs.pop("deadline", None)
self.deadline = deadline
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_deadline(self)
class NewDocumentModalForm(BaseModalForm):
""" Modal form for new documents
"""
title = forms.CharField(
label=_("Title"),
label_suffix=_(""),
max_length=500,
widget=forms.TextInput(
attrs={
"class": "form-control",
}
)
)
creation_date = forms.DateField(
label=_("Created on"),
label_suffix=_(""),
help_text=_("When has this file been created? Important for photos."),
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
file = forms.FileField(
label=_("File"),
label_suffix=_(""),
help_text=_("Allowed formats: pdf, jpg, png. Max size 15 MB."),
widget=forms.FileInput(
attrs={
"class": "form-control-file",
}
),
)
comment = forms.CharField(
required=False,
max_length=200,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
widget=forms.Textarea(
attrs={
"cols": 30,
"rows": 5,
"class": "form-control",
}
)
)
document_model = None
class Meta:
abstract = True
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Add new document")
self.form_caption = _("")
self.form_attrs = {
"enctype": "multipart/form-data", # important for file upload
}
if not self.document_model:
raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__))
def is_valid(self):
super_valid = super().is_valid()
_file = self.cleaned_data.get("file", None)
if _file is None or isinstance(_file, FieldFile):
# FieldFile declares that no new file has been uploaded and we do not need to check on the file again
return super_valid
mime_type_valid = self.document_model.is_mime_type_valid(_file)
if not mime_type_valid:
self.add_error(
"file",
FILE_TYPE_UNSUPPORTED
)
file_size_valid = self.document_model.is_file_size_valid(_file)
if not file_size_valid:
self.add_error(
"file",
FILE_SIZE_TOO_LARGE
)
file_valid = mime_type_valid and file_size_valid
return super_valid and file_valid
def save(self):
with transaction.atomic():
action = UserActionLogEntry.get_created_action(self.user)
edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document"))
doc = self.document_model.objects.create(
created=action,
title=self.cleaned_data["title"],
comment=self.cleaned_data["comment"],
file=self.cleaned_data["file"],
date_of_creation=self.cleaned_data["creation_date"],
instance=self.instance,
)
self.instance.log.add(edited_action)
self.instance.modified = edited_action
self.instance.save()
return doc
class EditDocumentModalForm(NewDocumentModalForm):
document = None
document_model = AbstractDocument
def __init__(self, *args, **kwargs):
self.document = kwargs.pop("document", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit document")
form_data = {
"title": self.document.title,
"comment": self.document.comment,
"creation_date": str(self.document.date_of_creation),
"file": self.document.file,
}
self.load_initial_data(form_data)
def save(self):
with transaction.atomic():
document = self.document
file = self.cleaned_data.get("file", None)
document.title = self.cleaned_data.get("title", None)
document.comment = self.cleaned_data.get("comment", None)
document.date_of_creation = self.cleaned_data.get("creation_date", None)
if not isinstance(file, FieldFile):
document.replace_file(file)
document.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=DOCUMENT_EDITED)
return document
class RecordModalForm(BaseModalForm):
""" Modal form for recording data
"""
confirm = forms.BooleanField(
label=_("Confirm record"),
label_suffix="",
widget=forms.CheckboxInput(),
required=True,
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Record data")
self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name)
# Disable automatic w-100 setting for this type of modal form. Looks kinda strange
self.fields["confirm"].widget.attrs["class"] = ""
if self.instance.recorded:
# unrecord!
self.fields["confirm"].label = _("Confirm unrecord")
self.form_title = _("Unrecord data")
self.form_caption = _("I, {} {}, confirm that this data must be unrecorded.").format(self.user.first_name, self.user.last_name)
if not isinstance(self.instance, RecordableObjectMixin):
raise NotImplementedError
def is_valid(self):
""" Checks for instance's validity and data quality
Returns:
"""
from intervention.models import Intervention
super_val = super().is_valid()
if self.instance.recorded:
# If user wants to unrecord an already recorded dataset, we do not need to perform custom checks
return super_val
checker = self.instance.quality_check()
for msg in checker.messages:
self.add_error(
"confirm",
msg
)
valid = checker.valid
# Special case: Intervention
# Add direct checks for related compensations
if isinstance(self.instance, Intervention):
comps_valid = self._are_compensations_valid()
valid = valid and comps_valid
return super_val and valid
def _are_deductions_valid(self):
""" Performs validity checks on deductions and their eco-account
Returns:
"""
deductions = self.instance.deductions.all()
for deduction in deductions:
checker = deduction.account.quality_check()
for msg in checker.messages:
self.add_error(
"confirm",
f"{deduction.account.identifier}: {msg}"
)
return checker.valid
return True
def _are_compensations_valid(self):
""" Runs a special case for intervention-compensations validity
Returns:
"""
comps = self.instance.compensations.filter(
deleted=None,
)
comps_valid = True
for comp in comps:
checker = comp.quality_check()
comps_valid = comps_valid and checker.valid
for msg in checker.messages:
self.add_error(
"confirm",
f"{comp.identifier}: {msg}"
)
deductions_valid = self._are_deductions_valid()
return comps_valid and deductions_valid
def save(self):
with transaction.atomic():
if self.cleaned_data["confirm"]:
if self.instance.recorded:
self.instance.set_unrecorded(self.user)
else:
self.instance.set_recorded(self.user)
return self.instance
def check_for_recorded_instance(self):
""" Overwrite the check method for doing nothing on the RecordModalForm
Returns:
"""
pass

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

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

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

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

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

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

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

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

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

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

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

@ -33,6 +33,7 @@ class Migration(migrations.Migration):
dependencies = [ dependencies = [
('konova', '0004_auto_20220209_0839'), ('konova', '0004_auto_20220209_0839'),
('compensation', '0002_auto_20220114_0936'),
] ]
operations = [ operations = [

@ -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,
},
),
]

@ -10,3 +10,4 @@ from .deadline import *
from .document import * from .document import *
from .geometry import * from .geometry import *
from .parcel import * from .parcel import *
from .resubmission import *

@ -744,3 +744,22 @@ class GeoReferencedMixin(models.Model):
x, x,
y, y,
) )
class ResubmitableObjectMixin(models.Model):
resubmissions = models.ManyToManyField(
"konova.Resubmission",
blank=True,
related_name="+",
)
class Meta:
abstract = True
def resubmit(self):
""" Run resubmit check and run for all related resubmissions
"""
resubmissions = self.resubmissions.all()
for resubmission in resubmissions:
resubmission.send_resubmission_mail(self.identifier)

@ -0,0 +1,46 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 15.08.22
"""
from dateutil.utils import today
from django.db import models
from konova.models import BaseResource
from konova.utils.mailer import Mailer
class Resubmission(BaseResource):
user = models.ForeignKey(
"user.User",
on_delete=models.CASCADE,
help_text="The user who wants to be notifed"
)
resubmit_on = models.DateField(
help_text="On which date the resubmission should be performed"
)
resubmission_sent = models.BooleanField(
default=False,
help_text="Whether a resubmission has been sent or not"
)
comment = models.TextField(
null=True,
blank=True,
help_text="Optional comment for the user itself"
)
def send_resubmission_mail(self, obj_identifier):
""" Sends a resubmission mail
"""
_today = today().date()
resubmission_handled = _today.__ge__(self.resubmit_on) and self.resubmission_sent
if resubmission_handled:
return
mailer = Mailer()
mailer.send_mail_resubmission(obj_identifier, self)
self.resubmission_sent = True
self.save()

@ -5,10 +5,9 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 01.09.21 Created on: 01.09.21
""" """
from django.http import FileResponse, HttpRequest, HttpResponse, Http404 from django.http import FileResponse, HttpRequest, Http404
from django.utils.translation import gettext_lazy as _
from konova.forms import RemoveModalForm from konova.forms.modals import RemoveModalForm
from konova.models import AbstractDocument from konova.models import AbstractDocument
from konova.utils.message_templates import DOCUMENT_REMOVED_TEMPLATE from konova.utils.message_templates import DOCUMENT_REMOVED_TEMPLATE

@ -398,3 +398,26 @@ class Mailer:
msg 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
)

Binary file not shown.

@ -18,15 +18,16 @@
#: konova/filters/mixins.py:277 konova/filters/mixins.py:323 #: konova/filters/mixins.py:277 konova/filters/mixins.py:323
#: konova/filters/mixins.py:361 konova/filters/mixins.py:362 #: konova/filters/mixins.py:361 konova/filters/mixins.py:362
#: konova/filters/mixins.py:393 konova/filters/mixins.py:394 #: konova/filters/mixins.py:393 konova/filters/mixins.py:394
#: konova/forms.py:179 konova/forms.py:281 konova/forms.py:395 #: konova/forms.py:183 konova/forms.py:285 konova/forms.py:399
#: konova/forms.py:439 konova/forms.py:449 konova/forms.py:462 #: konova/forms.py:443 konova/forms.py:453 konova/forms.py:466
#: konova/forms.py:474 konova/forms.py:492 user/forms.py:42 #: konova/forms.py:478 konova/forms.py:496 konova/forms.py:696
#: konova/forms.py:711 user/forms.py:42
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-08-10 08:37+0200\n" "POT-Creation-Date: 2022-08-15 09:39+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -85,7 +86,7 @@ msgstr "Bericht generieren"
msgid "Select a timespan and the desired conservation office" msgid "Select a timespan and the desired conservation office"
msgstr "Wählen Sie die Zeitspanne und die gewünschte Eintragungsstelle" 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" msgid "Continue"
msgstr "Weiter" msgstr "Weiter"
@ -241,7 +242,8 @@ msgstr ""
#: ema/templates/ema/detail/includes/states-after.html:36 #: ema/templates/ema/detail/includes/states-after.html:36
#: ema/templates/ema/detail/includes/states-before.html:36 #: ema/templates/ema/detail/includes/states-before.html:36
#: intervention/forms/modalForms.py:364 #: 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" msgid "Surface"
msgstr "Fläche" msgstr "Fläche"
@ -308,7 +310,8 @@ msgstr "Typ"
#: intervention/forms/modalForms.py:382 intervention/tables.py:87 #: intervention/forms/modalForms.py:382 intervention/tables.py:87
#: intervention/templates/intervention/detail/view.html:19 #: intervention/templates/intervention/detail/view.html:19
#: konova/templates/konova/includes/quickstart/interventions.html:4 #: 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 #: templates/navbars/navbar.html:22
msgid "Intervention" msgid "Intervention"
msgstr "Eingriff" msgstr "Eingriff"
@ -362,7 +365,7 @@ msgstr "Automatisch generiert"
#: intervention/templates/intervention/detail/includes/documents.html:28 #: intervention/templates/intervention/detail/includes/documents.html:28
#: intervention/templates/intervention/detail/view.html:31 #: intervention/templates/intervention/detail/view.html:31
#: intervention/templates/intervention/report/report.html:12 #: intervention/templates/intervention/report/report.html:12
#: konova/forms.py:438 #: konova/forms.py:442
msgid "Title" msgid "Title"
msgstr "Bezeichnung" msgstr "Bezeichnung"
@ -389,12 +392,13 @@ msgstr "Kompensation XY; Flur ABC"
#: intervention/templates/intervention/detail/includes/documents.html:34 #: intervention/templates/intervention/detail/includes/documents.html:34
#: intervention/templates/intervention/detail/includes/payments.html:34 #: intervention/templates/intervention/detail/includes/payments.html:34
#: intervention/templates/intervention/detail/includes/revocation.html:38 #: 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" msgid "Comment"
msgstr "Kommentar" msgstr "Kommentar"
#: compensation/forms/forms.py:59 compensation/forms/modalForms.py:471 #: 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" msgid "Additional comment"
msgstr "Zusätzlicher Kommentar" msgstr "Zusätzlicher Kommentar"
@ -479,7 +483,7 @@ msgstr "kompensiert Eingriff"
msgid "Select the intervention for which this compensation compensates" msgid "Select the intervention for which this compensation compensates"
msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist" 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" msgid "New compensation"
msgstr "Neue Kompensation" msgstr "Neue Kompensation"
@ -531,7 +535,7 @@ msgid "Due on which date"
msgstr "Zahlung wird an diesem Datum erwartet" msgstr "Zahlung wird an diesem Datum erwartet"
#: compensation/forms/modalForms.py:65 compensation/forms/modalForms.py:363 #: 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" msgid "Additional comment, maximum {} letters"
msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" msgstr "Zusätzlicher Kommentar, maximal {} Zeichen"
@ -576,7 +580,7 @@ msgstr "Neuer Zustand"
msgid "Insert data for the new state" msgid "Insert data for the new state"
msgstr "Geben Sie die Daten des neuen Zustandes ein" 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" msgid "Object removed"
msgstr "Objekt entfernt" msgstr "Objekt entfernt"
@ -602,7 +606,7 @@ msgstr "Fristart wählen"
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:36 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:36
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:36 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:36
#: ema/templates/ema/detail/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" msgid "Date"
msgstr "Datum" msgstr "Datum"
@ -850,24 +854,32 @@ msgstr "In LANIS öffnen"
msgid "Public report" msgid "Public report"
msgstr "Öffentlicher Bericht" msgstr "Öffentlicher Bericht"
#: compensation/templates/compensation/detail/compensation/includes/controls.html:17 #: compensation/templates/compensation/detail/compensation/includes/controls.html:15
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:31 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:15
#: ema/templates/ema/detail/includes/controls.html:31 #: ema/templates/ema/detail/includes/controls.html:15
#: intervention/templates/intervention/detail/includes/controls.html:36 #: 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" msgid "Edit"
msgstr "Bearbeiten" 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/compensation/includes/controls.html:24
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:38 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:38
#: ema/templates/ema/detail/includes/controls.html:38 #: ema/templates/ema/detail/includes/controls.html:38
#: intervention/templates/intervention/detail/includes/controls.html:43 #: 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 #: venv/lib/python3.7/site-packages/django/forms/formsets.py:391
msgid "Delete" msgid "Delete"
msgstr "Löschen" msgstr "Löschen"
@ -907,7 +919,7 @@ msgstr "Dokumente"
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:14 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:14
#: ema/templates/ema/detail/includes/documents.html:14 #: ema/templates/ema/detail/includes/documents.html:14
#: intervention/templates/intervention/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" msgid "Add new document"
msgstr "Neues Dokument hinzufügen" msgstr "Neues Dokument hinzufügen"
@ -915,7 +927,7 @@ msgstr "Neues Dokument hinzufügen"
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:31 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:31
#: ema/templates/ema/detail/includes/documents.html:31 #: ema/templates/ema/detail/includes/documents.html:31
#: intervention/templates/intervention/detail/includes/documents.html:31 #: intervention/templates/intervention/detail/includes/documents.html:31
#: konova/forms.py:448 #: konova/forms.py:452
msgid "Created on" msgid "Created on"
msgstr "Erstellt" msgstr "Erstellt"
@ -923,7 +935,7 @@ msgstr "Erstellt"
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:61 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:61
#: ema/templates/ema/detail/includes/documents.html:61 #: ema/templates/ema/detail/includes/documents.html:61
#: intervention/templates/intervention/detail/includes/documents.html:65 #: intervention/templates/intervention/detail/includes/documents.html:65
#: konova/forms.py:553 #: konova/forms.py:557
msgid "Edit document" msgid "Edit document"
msgstr "Dokument bearbeiten" msgstr "Dokument bearbeiten"
@ -1093,22 +1105,22 @@ msgstr ""
msgid "other users" msgid "other users"
msgstr "weitere Nutzer" msgstr "weitere Nutzer"
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:15 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:18
#: ema/templates/ema/detail/includes/controls.html:15 #: ema/templates/ema/detail/includes/controls.html:18
#: intervention/forms/modalForms.py:71 #: intervention/forms/modalForms.py:71
#: intervention/templates/intervention/detail/includes/controls.html:15 #: intervention/templates/intervention/detail/includes/controls.html:18
msgid "Share" msgid "Share"
msgstr "Freigabe" msgstr "Freigabe"
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:20 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:23
#: ema/templates/ema/detail/includes/controls.html:20 #: ema/templates/ema/detail/includes/controls.html:23
#: intervention/templates/intervention/detail/includes/controls.html:25 #: intervention/templates/intervention/detail/includes/controls.html:28
msgid "Unrecord" msgid "Unrecord"
msgstr "Entzeichnen" msgstr "Entzeichnen"
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:24 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:27
#: ema/templates/ema/detail/includes/controls.html:24 #: ema/templates/ema/detail/includes/controls.html:27
#: intervention/templates/intervention/detail/includes/controls.html:29 #: intervention/templates/intervention/detail/includes/controls.html:32
msgid "Record" msgid "Record"
msgstr "Verzeichnen" msgstr "Verzeichnen"
@ -1215,29 +1227,34 @@ msgstr ""
msgid "Responsible data" msgid "Responsible data"
msgstr "Daten zu den verantwortlichen Stellen" msgstr "Daten zu den verantwortlichen Stellen"
#: compensation/views/compensation.py:53 #: compensation/views/compensation.py:54
msgid "Compensations - Overview" msgid "Compensations - Overview"
msgstr "Kompensationen - Übersicht" 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" msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet" 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 #: ema/views.py:241 intervention/views.py:338
msgid "Edit {}" msgid "Edit {}"
msgstr "Bearbeite {}" msgstr "Bearbeite {}"
#: compensation/views/compensation.py:269 compensation/views/eco_account.py:360 #: compensation/views/compensation.py:270 compensation/views/eco_account.py:360
#: ema/views.py:195 intervention/views.py:542 #: ema/views.py:195 intervention/views.py:565
msgid "Log" msgid "Log"
msgstr "Log" msgstr "Log"
#: compensation/views/compensation.py:613 compensation/views/eco_account.py:728 #: compensation/views/compensation.py:614 compensation/views/eco_account.py:728
#: ema/views.py:559 intervention/views.py:688 #: ema/views.py:559 intervention/views.py:711
msgid "Report {}" msgid "Report {}"
msgstr "Bericht {}" 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 #: compensation/views/eco_account.py:65
msgid "Eco-account - Overview" msgid "Eco-account - Overview"
msgstr "Ökokonten - Übersicht" msgstr "Ökokonten - Übersicht"
@ -1255,12 +1272,12 @@ msgid "Eco-account removed"
msgstr "Ökokonto entfernt" msgstr "Ökokonto entfernt"
#: compensation/views/eco_account.py:381 ema/views.py:283 #: compensation/views/eco_account.py:381 ema/views.py:283
#: intervention/views.py:641 #: intervention/views.py:664
msgid "{} unrecorded" msgid "{} unrecorded"
msgstr "{} entzeichnet" msgstr "{} entzeichnet"
#: compensation/views/eco_account.py:381 ema/views.py:283 #: compensation/views/eco_account.py:381 ema/views.py:283
#: intervention/views.py:641 #: intervention/views.py:664
msgid "{} recorded" msgid "{} recorded"
msgstr "{} verzeichnet" msgstr "{} verzeichnet"
@ -1462,11 +1479,11 @@ msgid "Checked compensations data and payments"
msgstr "Kompensationen und Zahlungen geprüft" msgstr "Kompensationen und Zahlungen geprüft"
#: intervention/forms/modalForms.py:263 #: intervention/forms/modalForms.py:263
#: intervention/templates/intervention/detail/includes/controls.html:19 #: intervention/templates/intervention/detail/includes/controls.html:22
msgid "Run check" msgid "Run check"
msgstr "Prüfung vornehmen" msgstr "Prüfung vornehmen"
#: intervention/forms/modalForms.py:264 konova/forms.py:594 #: intervention/forms/modalForms.py:264 konova/forms.py:598
msgid "" msgid ""
"I, {} {}, confirm that all necessary control steps have been performed by " "I, {} {}, confirm that all necessary control steps have been performed by "
"myself." "myself."
@ -1622,11 +1639,11 @@ msgstr "Eingriff {} bearbeitet"
msgid "{} removed" msgid "{} removed"
msgstr "{} entfernt" msgstr "{} entfernt"
#: intervention/views.py:495 #: intervention/views.py:518
msgid "Check performed" msgid "Check performed"
msgstr "Prüfung durchgeführt" msgstr "Prüfung durchgeführt"
#: intervention/views.py:646 #: intervention/views.py:669
msgid "There are errors on this intervention:" msgid "There are errors on this intervention:"
msgstr "Es liegen Fehler in diesem Eingriff vor:" msgstr "Es liegen Fehler in diesem Eingriff vor:"
@ -1711,78 +1728,90 @@ msgstr "Nach Zulassungsbehörde suchen"
msgid "Search for conservation office" msgid "Search for conservation office"
msgstr "Nch Eintragungsstelle suchen" 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" msgid "Save"
msgstr "Speichern" msgstr "Speichern"
#: konova/forms.py:75 #: konova/forms.py:78
msgid "Not editable" msgid "Not editable"
msgstr "Nicht editierbar" msgstr "Nicht editierbar"
#: konova/forms.py:178 konova/forms.py:394 #: konova/forms.py:182 konova/forms.py:398
msgid "Confirm" msgid "Confirm"
msgstr "Bestätige" msgstr "Bestätige"
#: konova/forms.py:190 konova/forms.py:403 #: konova/forms.py:194 konova/forms.py:407
msgid "Remove" msgid "Remove"
msgstr "Löschen" msgstr "Löschen"
#: konova/forms.py:192 #: konova/forms.py:196
msgid "You are about to remove {} {}" msgid "You are about to remove {} {}"
msgstr "Sie sind dabei {} {} zu löschen" 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 #: templates/form/collapsable/form.html:45
msgid "Geometry" msgid "Geometry"
msgstr "Geometrie" msgstr "Geometrie"
#: konova/forms.py:331 #: konova/forms.py:335
msgid "Only surfaces allowed. Points or lines must be buffered." msgid "Only surfaces allowed. Points or lines must be buffered."
msgstr "" msgstr ""
"Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden." "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?" msgid "Are you sure?"
msgstr "Sind Sie sicher?" msgstr "Sind Sie sicher?"
#: konova/forms.py:450 #: konova/forms.py:454
msgid "When has this file been created? Important for photos." msgid "When has this file been created? Important for photos."
msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?" 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 #: venv/lib/python3.7/site-packages/django/db/models/fields/files.py:231
msgid "File" msgid "File"
msgstr "Datei" msgstr "Datei"
#: konova/forms.py:463 #: konova/forms.py:467
msgid "Allowed formats: pdf, jpg, png. Max size 15 MB." msgid "Allowed formats: pdf, jpg, png. Max size 15 MB."
msgstr "Formate: pdf, jpg, png. Maximal 15 MB." msgstr "Formate: pdf, jpg, png. Maximal 15 MB."
#: konova/forms.py:528 #: konova/forms.py:532
msgid "Added document" msgid "Added document"
msgstr "Dokument hinzugefügt" msgstr "Dokument hinzugefügt"
#: konova/forms.py:585 #: konova/forms.py:589
msgid "Confirm record" msgid "Confirm record"
msgstr "Verzeichnen bestätigen" msgstr "Verzeichnen bestätigen"
#: konova/forms.py:593 #: konova/forms.py:597
msgid "Record data" msgid "Record data"
msgstr "Daten verzeichnen" msgstr "Daten verzeichnen"
#: konova/forms.py:600 #: konova/forms.py:604
msgid "Confirm unrecord" msgid "Confirm unrecord"
msgstr "Entzeichnen bestätigen" msgstr "Entzeichnen bestätigen"
#: konova/forms.py:601 #: konova/forms.py:605
msgid "Unrecord data" msgid "Unrecord data"
msgstr "Daten entzeichnen" msgstr "Daten entzeichnen"
#: konova/forms.py:602 #: konova/forms.py:606
msgid "I, {} {}, confirm that this data must be unrecorded." msgid "I, {} {}, confirm that this data must be unrecorded."
msgstr "" msgstr ""
"Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen." "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 #: konova/management/commands/setup_data.py:26
msgid "On shared access gained" msgid "On shared access gained"
msgstr "Wenn mir eine Freigabe zu Daten erteilt wird" msgstr "Wenn mir eine Freigabe zu Daten erteilt wird"
@ -1929,7 +1958,7 @@ msgstr "{} - Freigegebene Daten verzeichnet"
msgid "{} - Shared data checked" msgid "{} - Shared data checked"
msgstr "{} - Freigegebene Daten geprüft" 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" msgid "{} - Deduction changed"
msgstr "{} - Abbuchung geändert" msgstr "{} - Abbuchung geändert"
@ -1937,10 +1966,14 @@ msgstr "{} - Abbuchung geändert"
msgid "{} - Shared data deleted" msgid "{} - Shared data deleted"
msgstr "{} - Freigegebene Daten gelöscht" 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" msgid "Request for new API token"
msgstr "Anfrage für neuen API Token" msgstr "Anfrage für neuen API Token"
#: konova/utils/mailer.py:420
msgid "Resubmission - {}"
msgstr "Wiedervorlage - {}"
#: konova/utils/message_templates.py:10 #: konova/utils/message_templates.py:10
msgid "no further details" msgid "no further details"
msgstr "keine weitere Angabe" msgstr "keine weitere Angabe"
@ -2223,11 +2256,11 @@ msgstr "Irgendetwas ist passiert. Wir arbeiten daran!"
msgid "Hello support" msgid "Hello support"
msgstr "Hallo 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" msgid "you need to verify the API token for user"
msgstr "Sie müssen einen API Token für folgenden Nutzer freischalten" 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 "" msgid ""
"If unsure, please contact the user. The API token can not be used until you " "If unsure, please contact the user. The API token can not be used until you "
"activated it in the admin backend." "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 " "Token kann so lange nicht verwendet werden, wie er noch nicht von Ihnen im "
"Admin Backend aktiviert worden ist." "Admin Backend aktiviert worden ist."
#: templates/email/api/verify_token.html:18 #: templates/email/api/verify_token.html:19
#: templates/email/checking/shared_data_checked.html:19 #: templates/email/checking/shared_data_checked.html:20
#: templates/email/checking/shared_data_checked_team.html:19 #: templates/email/checking/shared_data_checked_team.html:20
#: templates/email/deleting/shared_data_deleted.html:19 #: templates/email/deleting/shared_data_deleted.html:20
#: templates/email/deleting/shared_data_deleted_team.html:19 #: templates/email/deleting/shared_data_deleted_team.html:20
#: templates/email/other/deduction_changed.html:38 #: templates/email/other/deduction_changed.html:41
#: templates/email/recording/shared_data_recorded.html:19 #: templates/email/other/deduction_changed_team.html:41
#: templates/email/recording/shared_data_recorded_team.html:19 #: templates/email/recording/shared_data_recorded.html:20
#: templates/email/recording/shared_data_unrecorded.html:19 #: templates/email/recording/shared_data_recorded_team.html:20
#: templates/email/recording/shared_data_unrecorded_team.html:19 #: templates/email/recording/shared_data_unrecorded.html:20
#: templates/email/sharing/shared_access_given.html:20 #: templates/email/recording/shared_data_unrecorded_team.html:20
#: templates/email/sharing/shared_access_given_team.html:20 #: templates/email/resubmission/resubmission.html:21
#: templates/email/sharing/shared_access_removed.html:20 #: templates/email/sharing/shared_access_given.html:21
#: templates/email/sharing/shared_access_removed_team.html:20 #: 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" msgid "Best regards"
msgstr "Beste Grüße" msgstr "Beste Grüße"
@ -2263,18 +2298,19 @@ msgstr "Freigegebene Daten geprüft"
#: templates/email/other/deduction_changed.html:8 #: templates/email/other/deduction_changed.html:8
#: templates/email/recording/shared_data_recorded.html:8 #: templates/email/recording/shared_data_recorded.html:8
#: templates/email/recording/shared_data_unrecorded.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_given.html:8
#: templates/email/sharing/shared_access_removed.html:8 #: templates/email/sharing/shared_access_removed.html:8
msgid "Hello " msgid "Hello "
msgstr "Hallo " msgstr "Hallo "
#: templates/email/checking/shared_data_checked.html:10 #: templates/email/checking/shared_data_checked.html:11
#: templates/email/checking/shared_data_checked_team.html:10 #: templates/email/checking/shared_data_checked_team.html:11
msgid "the following dataset has just been checked" msgid "the following dataset has just been checked"
msgstr "der folgende Datensatz wurde soeben geprüft " msgstr "der folgende Datensatz wurde soeben geprüft "
#: templates/email/checking/shared_data_checked.html:16 #: templates/email/checking/shared_data_checked.html:17
#: templates/email/checking/shared_data_checked_team.html:16 #: templates/email/checking/shared_data_checked_team.html:17
msgid "" msgid ""
"This means, the responsible registration office just confirmed the " "This means, the responsible registration office just confirmed the "
"correctness of this dataset." "correctness of this dataset."
@ -2284,6 +2320,7 @@ msgstr ""
#: templates/email/checking/shared_data_checked_team.html:8 #: templates/email/checking/shared_data_checked_team.html:8
#: templates/email/deleting/shared_data_deleted_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_recorded_team.html:8
#: templates/email/recording/shared_data_unrecorded_team.html:8 #: templates/email/recording/shared_data_unrecorded_team.html:8
#: templates/email/sharing/shared_access_given_team.html:8 #: templates/email/sharing/shared_access_given_team.html:8
@ -2296,14 +2333,15 @@ msgstr "Hallo Team"
msgid "Shared data deleted" msgid "Shared data deleted"
msgstr "Freigegebene Daten gelöscht" msgstr "Freigegebene Daten gelöscht"
#: templates/email/deleting/shared_data_deleted.html:10 #: templates/email/deleting/shared_data_deleted.html:11
#: templates/email/deleting/shared_data_deleted_team.html:10 #: templates/email/deleting/shared_data_deleted_team.html:11
msgid "the following dataset has just been deleted" msgid "the following dataset has just been deleted"
msgstr "der folgende Datensatz wurde soeben gelöscht " msgstr "der folgende Datensatz wurde soeben gelöscht "
#: templates/email/deleting/shared_data_deleted.html:16 #: templates/email/deleting/shared_data_deleted.html:17
#: templates/email/deleting/shared_data_deleted_team.html:16 #: templates/email/deleting/shared_data_deleted_team.html:17
#: templates/email/other/deduction_changed.html:35 #: templates/email/other/deduction_changed.html:38
#: templates/email/other/deduction_changed_team.html:38
msgid "" msgid ""
"If this should not have been happened, please contact us. See the signature " "If this should not have been happened, please contact us. See the signature "
"for details." "for details."
@ -2312,27 +2350,33 @@ msgstr ""
"mail Signatur finden Sie weitere Kontaktinformationen." "mail Signatur finden Sie weitere Kontaktinformationen."
#: templates/email/other/deduction_changed.html:4 #: templates/email/other/deduction_changed.html:4
#: templates/email/other/deduction_changed_team.html:4
msgid "Deduction changed" msgid "Deduction changed"
msgstr "Abbuchung geändert" 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:" msgid "a deduction of this eco account has changed:"
msgstr "eine Abbuchung des Ökokontos hat sich geändert:" 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" msgid "Attribute"
msgstr "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" msgid "Old"
msgstr "Alt" 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 #: templates/generic_index.html:43 user/templates/user/team/index.html:22
msgid "New" msgid "New"
msgstr "Neu" 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" msgid "EcoAccount"
msgstr "Ökokonto" msgstr "Ökokonto"
@ -2341,19 +2385,19 @@ msgstr "Ökokonto"
msgid "Shared data recorded" msgid "Shared data recorded"
msgstr "Freigegebene Daten verzeichnet" msgstr "Freigegebene Daten verzeichnet"
#: templates/email/recording/shared_data_recorded.html:10 #: templates/email/recording/shared_data_recorded.html:11
#: templates/email/recording/shared_data_recorded_team.html:10 #: templates/email/recording/shared_data_recorded_team.html:11
msgid "the following dataset has just been recorded" msgid "the following dataset has just been recorded"
msgstr "der folgende Datensatz wurde soeben verzeichnet " msgstr "der folgende Datensatz wurde soeben verzeichnet "
#: templates/email/recording/shared_data_recorded.html:16 #: templates/email/recording/shared_data_recorded.html:17
#: templates/email/recording/shared_data_recorded_team.html:16 #: templates/email/recording/shared_data_recorded_team.html:17
msgid "This means the data is now publicly available, e.g. in LANIS" msgid "This means the data is now publicly available, e.g. in LANIS"
msgstr "" msgstr ""
"Das bedeutet, dass die Daten nun öffentlich verfügbar sind, z.B. im LANIS." "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.html:27
#: templates/email/recording/shared_data_recorded_team.html:26 #: templates/email/recording/shared_data_recorded_team.html:27
msgid "" msgid ""
"Please note: Recorded intervention means the compensations are recorded as " "Please note: Recorded intervention means the compensations are recorded as "
"well." "well."
@ -2366,18 +2410,18 @@ msgstr ""
msgid "Shared data unrecorded" msgid "Shared data unrecorded"
msgstr "Freigegebene Daten entzeichnet" msgstr "Freigegebene Daten entzeichnet"
#: templates/email/recording/shared_data_unrecorded.html:10 #: templates/email/recording/shared_data_unrecorded.html:11
#: templates/email/recording/shared_data_unrecorded_team.html:10 #: templates/email/recording/shared_data_unrecorded_team.html:11
msgid "the following dataset has just been unrecorded" msgid "the following dataset has just been unrecorded"
msgstr "der folgende Datensatz wurde soeben entzeichnet " msgstr "der folgende Datensatz wurde soeben entzeichnet "
#: templates/email/recording/shared_data_unrecorded.html:16 #: templates/email/recording/shared_data_unrecorded.html:17
#: templates/email/recording/shared_data_unrecorded_team.html:16 #: templates/email/recording/shared_data_unrecorded_team.html:17
msgid "This means the data is no longer publicly available." msgid "This means the data is no longer publicly available."
msgstr "Das bedeutet, dass die Daten nicht länger öffentlich verfügbar sind." 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.html:27
#: templates/email/recording/shared_data_unrecorded_team.html:26 #: templates/email/recording/shared_data_unrecorded_team.html:27
msgid "" msgid ""
"Please note: Unrecorded intervention means the compensations are unrecorded " "Please note: Unrecorded intervention means the compensations are unrecorded "
"as well." "as well."
@ -2385,22 +2429,30 @@ msgstr ""
"Bitte beachten Sie: Entzeichnete Eingriffe bedeuten, dass auch die " "Bitte beachten Sie: Entzeichnete Eingriffe bedeuten, dass auch die "
"zugehörigen Kompensationen automatisch entzeichnet worden sind." "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.html:4
#: templates/email/sharing/shared_access_given_team.html:4 #: templates/email/sharing/shared_access_given_team.html:4
msgid "Access shared" msgid "Access shared"
msgstr "Zugriff freigegeben" 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" msgid "the following dataset has just been shared with you"
msgstr "der folgende Datensatz wurde soeben für Sie freigegeben " msgstr "der folgende Datensatz wurde soeben für Sie freigegeben "
#: templates/email/sharing/shared_access_given.html:16 #: templates/email/sharing/shared_access_given.html:17
#: templates/email/sharing/shared_access_given_team.html:16 #: templates/email/sharing/shared_access_given_team.html:17
msgid "This means you can now edit this dataset." msgid "This means you can now edit this dataset."
msgstr "Das bedeutet, dass Sie diesen Datensatz nun auch bearbeiten können." 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.html:18
#: templates/email/sharing/shared_access_given_team.html:17 #: templates/email/sharing/shared_access_given_team.html:18
msgid "" msgid ""
"The shared dataset appears now by default on your overview for this dataset " "The shared dataset appears now by default on your overview for this dataset "
"type." "type."
@ -2408,8 +2460,8 @@ msgstr ""
"Der freigegebene Datensatz ist nun standardmäßig in Ihrer Übersicht für den " "Der freigegebene Datensatz ist nun standardmäßig in Ihrer Übersicht für den "
"Datensatztyp im KSP gelistet." "Datensatztyp im KSP gelistet."
#: templates/email/sharing/shared_access_given.html:27 #: templates/email/sharing/shared_access_given.html:28
#: templates/email/sharing/shared_access_given_team.html:27 #: templates/email/sharing/shared_access_given_team.html:28
msgid "" msgid ""
"Please note: Shared access on an intervention means you automatically have " "Please note: Shared access on an intervention means you automatically have "
"editing access to related compensations." "editing access to related compensations."
@ -2418,7 +2470,7 @@ msgstr ""
"Sie automatisch auch Zugriff auf die zugehörigen Kompensationen erhalten " "Sie automatisch auch Zugriff auf die zugehörigen Kompensationen erhalten "
"haben." "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" msgid "the following dataset has just been shared with your team"
msgstr "der folgende Datensatz wurde soeben für Ihr Team freigegeben " 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" msgid "Shared access removed"
msgstr "Freigegebener Zugriff entzogen" msgstr "Freigegebener Zugriff entzogen"
#: templates/email/sharing/shared_access_removed.html:10 #: templates/email/sharing/shared_access_removed.html:11
msgid "" msgid ""
"your shared access, including editing, has been revoked for the dataset " "your shared access, including editing, has been revoked for the dataset "
msgstr "" msgstr ""
"Ihnen wurde soeben der bearbeitende Zugriff auf den folgenden Datensatz " "Ihnen wurde soeben der bearbeitende Zugriff auf den folgenden Datensatz "
"entzogen: " "entzogen: "
#: templates/email/sharing/shared_access_removed.html:16 #: templates/email/sharing/shared_access_removed.html:17
#: templates/email/sharing/shared_access_removed_team.html:16 #: templates/email/sharing/shared_access_removed_team.html:17
msgid "However, you are still able to view the dataset content." msgid "However, you are still able to view the dataset content."
msgstr "Sie können den Datensatz aber immer noch im KSP einsehen." 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.html:18
#: templates/email/sharing/shared_access_removed_team.html:17 #: templates/email/sharing/shared_access_removed_team.html:18
msgid "" msgid ""
"Please use the provided search filter on the dataset`s overview pages to " "Please use the provided search filter on the dataset`s overview pages to "
"find them." "find them."
@ -2448,7 +2500,7 @@ msgstr ""
"Nutzen Sie hierzu einfach die entsprechenden Suchfilter auf den " "Nutzen Sie hierzu einfach die entsprechenden Suchfilter auf den "
"Übersichtsseiten" "Übersichtsseiten"
#: templates/email/sharing/shared_access_removed_team.html:10 #: templates/email/sharing/shared_access_removed_team.html:11
msgid "" msgid ""
"your teams shared access, including editing, has been revoked for the " "your teams shared access, including editing, has been revoked for the "
"dataset " "dataset "

@ -0,0 +1,29 @@
{% load i18n %}
<div>
<h2>{% trans 'Resubmission' %}</h2>
<h4>{{obj_identifier}}</h4>
<hr>
<article>
{% trans 'Hello ' %} {{resubmission.user.username}},
<br>
<br>
{% trans 'you wanted to be reminded on this entry.' %}
<br>
{% if resubmission.comment %}
<br>
{% trans 'Your personal comment:' %}
<br>
<article style="font: italic">"{{resubmission.comment}}"</article>
{% endif %}
<br>
<br>
{% trans 'Best regards' %}
<br>
KSP
<br>
<br>
{% include 'email/signature.html' %}
</article>
</div>

@ -15,7 +15,8 @@ from api.models import APIUserToken
from intervention.inputs import GenerateInput from intervention.inputs import GenerateInput
from user.models import User, UserNotification, Team 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): class UserNotificationForm(BaseForm):

@ -0,0 +1,18 @@
# Generated by Django 3.1.3 on 2022-08-15 05:59
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0005_team_deleted'),
]
operations = [
migrations.AlterField(
model_name='usernotification',
name='id',
field=models.CharField(choices=[('NOTIFY_ON_SHARED_ACCESS_REMOVED', 'NOTIFY_ON_SHARED_ACCESS_REMOVED'), ('NOTIFY_ON_SHARED_DATA_RECORDED', 'NOTIFY_ON_SHARED_DATA_RECORDED'), ('NOTIFY_ON_SHARED_DATA_DELETED', 'NOTIFY_ON_SHARED_DATA_DELETED'), ('NOTIFY_ON_SHARED_DATA_CHECKED', 'NOTIFY_ON_SHARED_DATA_CHECKED'), ('NOTIFY_ON_SHARED_ACCESS_GAINED', 'NOTIFY_ON_SHARED_ACCESS_GAINED'), ('NOTIFY_ON_DEDUCTION_CHANGES', 'NOTIFY_ON_DEDUCTION_CHANGES')], max_length=500, primary_key=True, serialize=False),
),
]
Loading…
Cancel
Save