Docker Update #199

Merged
mpeltriaux merged 13 commits from master into Docker 2 years ago

@ -20,7 +20,7 @@ from compensation.models import CompensationDocument, EcoAccountDocument
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \
CompensationStateTreeRadioSelect
from konova.contexts import BaseContext
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
from konova.forms.modals import BaseModalForm, NewDocumentModalForm, RemoveModalForm
from konova.models import DeadlineType
from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, \
ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED

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

@ -14,21 +14,22 @@ from user.models import User, Team
from django.db import models, transaction
from django.db.models import QuerySet, Sum
from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from compensation.managers import CompensationManager
from compensation.models import CompensationState, CompensationAction
from compensation.utils.quality import CompensationQualityChecker
from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \
GeoReferencedMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
GeoReferencedMixin, DeadlineType, ResubmitableObjectMixin
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \
DOCUMENT_REMOVED_TEMPLATE, COMPENSATION_EDITED_TEMPLATE, DEADLINE_REMOVED, ADDED_DEADLINE, \
DOCUMENT_REMOVED_TEMPLATE, DEADLINE_REMOVED, ADDED_DEADLINE, \
COMPENSATION_ACTION_REMOVED, COMPENSATION_STATE_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
from user.models import UserActionLogEntry
class AbstractCompensation(BaseObject, GeoReferencedMixin):
class AbstractCompensation(BaseObject,
GeoReferencedMixin,
ResubmitableObjectMixin
):
"""
Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation,
EMA or EcoAccount.
@ -226,6 +227,15 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
request = self.set_geometry_conflict_message(request)
return request
def get_finished_deadlines(self):
""" Getter for FINISHED-deadlines
Returns:
queryset (QuerySet): The finished deadlines
"""
return self.deadlines.filter(
type=DeadlineType.FINISHED
)
class CEFMixin(models.Model):
""" Provides CEF flag as Mixin

@ -21,6 +21,7 @@ from compensation.models.compensation import AbstractCompensation, PikMixin
from compensation.utils.quality import EcoAccountQualityChecker
from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \
generate_document_file_upload_path
from konova.tasks import celery_send_mail_deduction_changed, celery_send_mail_deduction_changed_team
class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin):
@ -161,6 +162,25 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
"""
return reverse("compensation:acc:share", args=(self.id, self.access_token))
def send_notification_mail_on_deduction_change(self, data_change: dict):
""" Sends notification mails for changes on the deduction
Args:
data_change ():
Returns:
"""
# Send mail
shared_users = self.shared_users.values_list("id", flat=True)
for user_id in shared_users:
celery_send_mail_deduction_changed.delay(self.identifier, self.title, user_id, data_change)
# Send mail
shared_teams = self.shared_teams.values_list("id", flat=True)
for team_id in shared_teams:
celery_send_mail_deduction_changed_team.delay(self.identifier, self.title, team_id, data_change)
class EcoAccountDocument(AbstractDocument):
"""
@ -251,4 +271,4 @@ class EcoAccountDeduction(BaseResource):
if user is not None:
self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
super().delete(*args, **kwargs)
super().delete(*args, **kwargs)

@ -12,6 +12,9 @@
</button>
</a>
{% 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 %}
<a href="{% url 'compensation:edit' obj.id %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}">

@ -20,6 +20,11 @@
</div>
</div>
</div>
{% if not has_finished_deadlines %}
<div class="alert alert-danger mb-0">
{% trans 'Missing finished deadline ' %}
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>

@ -12,6 +12,9 @@
</button>
</a>
{% 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 %}">
{% fa5_icon 'share-alt' %}
</button>

@ -20,6 +20,11 @@
</div>
</div>
</div>
{% if not has_finished_deadlines %}
<div class="alert alert-danger mb-0">
{% trans 'Missing finished deadline ' %}
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>

@ -31,6 +31,7 @@ urlpatterns = [
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>/report', report_view, name='report'),
path('<id>/resub', create_resubmission_view, name='resubmission-create'),
# Documents
path('<id>/document/new/', new_document_view, name='new-doc'),

@ -19,6 +19,7 @@ urlpatterns = [
path('<id>/report', report_view, name='report'),
path('<id>/edit', edit_view, name='edit'),
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/<state_id>/edit', state_edit_view, name='state-edit'),

@ -19,6 +19,7 @@ class CompensationQualityChecker(AbstractQualityChecker):
self._check_states()
self._check_actions()
self._check_geometry()
self._check_deadlines()
self.valid = len(self.messages) == 0
def _check_states(self):
@ -47,6 +48,16 @@ class CompensationQualityChecker(AbstractQualityChecker):
if not self.obj.actions.all():
self._add_missing_attr_name(_con("Compensation", "Actions"))
def _check_deadlines(self):
""" Checks data quality for related Deadline objects
Returns:
"""
finished_deadlines = self.obj.get_finished_deadlines()
if not finished_deadlines.exists():
self._add_missing_attr_name(_("Finished deadlines"))
class EcoAccountQualityChecker(CompensationQualityChecker):
def run_check(self):

@ -14,7 +14,9 @@ from compensation.tables import CompensationTable
from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.decorators import *
from konova.forms import RemoveModalForm, SimpleGeomForm, RemoveDeadlineModalForm, EditDocumentModalForm
from konova.forms.modals import RemoveModalForm,RemoveDeadlineModalForm, EditDocumentModalForm, \
ResubmissionModalForm
from konova.forms import SimpleGeomForm
from konova.models import Deadline
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.documents import get_document, remove_document
@ -240,6 +242,7 @@ def detail_view(request: HttpRequest, id: str):
"is_ets_member": in_group(_user, ETS_GROUP),
"LANIS_LINK": comp.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}",
"has_finished_deadlines": comp.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
@ -655,3 +658,26 @@ def report_view(request: HttpRequest, id: str):
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def create_resubmission_view(request: HttpRequest, id: str):
""" Renders resubmission form for a compensation
Args:
request (HttpRequest): The incoming request
id (str): Compensation's id
Returns:
"""
com = get_object_or_404(Compensation, id=id)
form = ResubmissionModalForm(request.POST or None, instance=com, request=request)
form.action_url = reverse("compensation:resubmission-create", args=(id,))
return form.process_request(
request,
msg_success=_("Resubmission set"),
redirect_url=reverse("compensation:detail", args=(id,))
)

@ -25,14 +25,15 @@ from intervention.forms.modalForms import NewDeductionModalForm, ShareModalForm,
from konova.contexts import BaseContext
from konova.decorators import any_group_check, default_group_required, conservation_office_group_required, \
shared_access_required
from konova.forms import RemoveModalForm, SimpleGeomForm, NewDocumentModalForm, RecordModalForm, \
RemoveDeadlineModalForm, EditDocumentModalForm
from konova.forms.modals import RemoveModalForm, RecordModalForm, \
RemoveDeadlineModalForm, EditDocumentModalForm, ResubmissionModalForm
from konova.forms import SimpleGeomForm
from konova.models import Deadline
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.documents import get_document, remove_document
from konova.utils.generators import generate_qr_code
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, DATA_UNSHARED, DATA_UNSHARED_EXPLANATION, \
from konova.utils.message_templates import IDENTIFIER_REPLACED, FORM_INVALID, \
CANCEL_ACC_RECORDED_OR_DEDUCTED, DEDUCTION_REMOVED, DEDUCTION_ADDED, DOCUMENT_ADDED, COMPENSATION_STATE_REMOVED, \
COMPENSATION_STATE_ADDED, COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_ADDED, DEADLINE_ADDED, DEADLINE_REMOVED, \
DEDUCTION_EDITED, DOCUMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED, \
@ -242,6 +243,7 @@ def detail_view(request: HttpRequest, id: str):
"deductions": deductions,
"actions": actions,
TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}",
"has_finished_deadlines": acc.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
@ -837,4 +839,27 @@ def create_share_view(request: HttpRequest, id: str):
return form.process_request(
request,
msg_success=_("Share settings updated")
)
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def create_resubmission_view(request: HttpRequest, id: str):
""" Renders resubmission form for an eco account
Args:
request (HttpRequest): The incoming request
id (str): EcoAccount's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
form = ResubmissionModalForm(request.POST or None, instance=acc, request=request)
form.action_url = reverse("compensation:acc:resubmission-create", args=(id,))
return form.process_request(
request,
msg_success=_("Resubmission set"),
redirect_url=reverse("compensation:acc:detail", args=(id,))
)

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

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

@ -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>
</a>
{% 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 %}">
{% fa5_icon 'share-alt' %}
</button>

@ -20,6 +20,11 @@
</div>
</div>
</div>
{% if not has_finished_deadlines %}
<div class="alert alert-danger mb-0">
{% trans 'Missing finished deadline ' %}
</div>
{% endif %}
<div class="card-body scroll-300 p-2">
<table class="table table-hover">
<thead>

@ -19,6 +19,7 @@ urlpatterns = [
path('<id>/remove', remove_view, name='remove'),
path('<id>/record', record_view, name='record'),
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/<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.decorators import conservation_office_group_required, shared_access_required
from ema.models import Ema, EmaDocument
from konova.forms import RemoveModalForm, SimpleGeomForm, RecordModalForm, RemoveDeadlineModalForm, \
EditDocumentModalForm
from konova.forms.modals import RemoveModalForm, RecordModalForm, RemoveDeadlineModalForm, \
EditDocumentModalForm, ResubmissionModalForm
from konova.forms import SimpleGeomForm
from konova.models import Deadline
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
@ -166,6 +167,7 @@ def detail_view(request: HttpRequest, id: str):
"is_ets_member": in_group(_user, ETS_GROUP),
"LANIS_LINK": ema.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}",
"has_finished_deadlines": ema.get_finished_deadlines().exists(),
}
context = BaseContext(request, context).context
return render(request, template, context)
@ -709,4 +711,27 @@ def deadline_remove_view(request: HttpRequest, id: str, deadline_id: str):
request,
msg_success=DEADLINE_REMOVED,
redirect_url=reverse("ema:detail", args=(id,)) + "#related_data"
)
)
@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 django import forms
from konova.forms.base_form import BaseForm
from konova.utils.message_templates import EDITED_GENERAL_DATA
from user.models import User
from django.db import transaction
@ -19,7 +20,7 @@ from codelist.settings import CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID, \
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
from intervention.inputs import GenerateInput
from intervention.models import Intervention, Legal, Responsibility, Handler
from konova.forms import BaseForm, SimpleGeomForm
from konova.forms.geometry_form import SimpleGeomForm
from user.models import UserActionLogEntry

@ -19,7 +19,8 @@ from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount, EcoAccountDeduction
from intervention.inputs import TextToClipboardInput
from intervention.models import Intervention, InterventionDocument, RevocationDocument
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
from konova.forms.modals import BaseModalForm
from konova.forms.modals import NewDocumentModalForm, RemoveModalForm
from konova.utils.general import format_german_float
from konova.utils.user_checks import is_default_group_only
@ -508,28 +509,44 @@ class EditEcoAccountDeductionModalForm(NewDeductionModalForm):
deduction = self.deduction
form_account = self.cleaned_data.get("account", None)
form_intervention = self.cleaned_data.get("intervention", None)
current_account = deduction.account
current_intervention = deduction.intervention
old_account = deduction.account
old_intervention = deduction.intervention
old_surface = deduction.surface
# If account or intervention has been changed, we put that change in the logs just as if the deduction has
# been removed for this entry. Act as if the deduction is newly created for the new entries
if current_account != form_account:
current_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED)
if old_account != form_account:
old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED)
form_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED)
else:
current_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED)
old_account.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED)
if current_intervention != form_intervention:
current_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED)
if old_intervention != form_intervention:
old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_REMOVED)
form_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_ADDED)
else:
current_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED)
old_intervention.mark_as_edited(self.user, self.request, edit_comment=DEDUCTION_EDITED)
deduction.account = form_account
deduction.intervention = self.cleaned_data.get("intervention", None)
deduction.surface = self.cleaned_data.get("surface", None)
deduction.save()
data_changes = {
"surface": {
"old": old_surface,
"new": deduction.surface,
},
"intervention": {
"old": old_intervention.identifier,
"new": deduction.intervention.identifier,
},
"account": {
"old": old_account.identifier,
"new": deduction.account.identifier,
}
}
old_account.send_notification_mail_on_deduction_change(data_changes)
return deduction

@ -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 konova.models import generate_document_file_upload_path, AbstractDocument, BaseObject, \
ShareableObjectMixin, \
RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin
from konova.settings import LANIS_LINK_TEMPLATE, LANIS_ZOOM_LUT, DEFAULT_SRID_RLP
RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin, ResubmitableObjectMixin
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE, \
PAYMENT_REMOVED, PAYMENT_ADDED, REVOCATION_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
from user.models import UserActionLogEntry
class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, CheckableObjectMixin, GeoReferencedMixin):
class Intervention(BaseObject,
ShareableObjectMixin,
RecordableObjectMixin,
CheckableObjectMixin,
GeoReferencedMixin,
ResubmitableObjectMixin
):
"""
Interventions are e.g. construction sites where nature used to be.
"""

@ -12,6 +12,9 @@
</button>
</a>
{% 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 %}">
{% fa5_icon 'share-alt' %}
</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, \
create_share_view, remove_revocation_view, new_revocation_view, check_view, log_view, new_deduction_view, \
record_view, remove_document_view, get_document_view, get_revocation_view, new_id_view, report_view, \
remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view
remove_deduction_view, remove_compensation_view, edit_deduction_view, edit_revocation_view, edit_document_view, \
create_resubmission_view
app_name = "intervention"
urlpatterns = [
@ -26,6 +27,7 @@ urlpatterns = [
path('<id>/check', check_view, name='check'),
path('<id>/record', record_view, name='record'),
path('<id>/report', report_view, name='report'),
path('<id>/resub', create_resubmission_view, name='resubmission-create'),
# Compensations
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 konova.contexts import BaseContext
from konova.decorators import *
from konova.forms import SimpleGeomForm, RemoveModalForm, RecordModalForm, EditDocumentModalForm
from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm, RecordModalForm, EditDocumentModalForm, ResubmissionModalForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.documents import remove_document, get_document
from konova.utils.generators import generate_qr_code
@ -475,6 +476,29 @@ def create_share_view(request: HttpRequest, id: str):
)
@login_required
@default_group_required
@shared_access_required(Intervention, "id")
def create_resubmission_view(request: HttpRequest, id: str):
""" Renders resubmission form for an intervention
Args:
request (HttpRequest): The incoming request
id (str): Intervention's id
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
form = ResubmissionModalForm(request.POST or None, instance=intervention, request=request)
form.action_url = reverse("intervention:resubmission-create", args=(id,))
return form.process_request(
request,
msg_success=_("Resubmission set"),
redirect_url=reverse("intervention:detail", args=(id,))
)
@login_required
@registration_office_group_required
@shared_access_required(Intervention, "id")

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

@ -1,687 +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 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 = isinstance(
self,
(
NewDeductionModalForm,
EditEcoAccountDeductionModalForm,
RemoveEcoAccountDeductionModalForm,
)
)
if is_none or is_other_data_type or is_deduction_form:
# 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)

@ -28,4 +28,5 @@ USER_NOTIFICATIONS_NAMES = {
"NOTIFY_ON_SHARED_DATA_RECORDED": _("On shared data recorded"),
"NOTIFY_ON_SHARED_DATA_DELETED": _("On shared data deleted"),
"NOTIFY_ON_SHARED_DATA_CHECKED": _("On shared data checked"),
}
"NOTIFY_ON_DEDUCTION_CHANGES": _("On deduction changes"),
}

@ -33,6 +33,7 @@ class Migration(migrations.Migration):
dependencies = [
('konova', '0004_auto_20220209_0839'),
('compensation', '0002_auto_20220114_0936'),
]
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 .geometry import *
from .parcel import *
from .resubmission import *

@ -743,4 +743,23 @@ class GeoReferencedMixin(models.Model):
zoom_lvl,
x,
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()

@ -106,3 +106,17 @@ def celery_send_mail_shared_data_checked_team(obj_identifier, obj_title=None, te
from user.models import Team
team = Team.objects.get(id=team_id)
team.send_mail_shared_data_checked(obj_identifier, obj_title)
@shared_task
def celery_send_mail_deduction_changed_team(obj_identifier, obj_title=None, team_id=None, data_changes=None):
from user.models import Team
team = Team.objects.get(id=team_id)
team.send_mail_deduction_changed(obj_identifier, obj_title, data_changes)
@shared_task
def celery_send_mail_deduction_changed(obj_identifier, obj_title=None, user_id=None, data_changes=None):
from user.models import User
user = User.objects.get(id=user_id)
user.send_mail_deduction_changed(obj_identifier, obj_title, data_changes)

@ -22,7 +22,7 @@ from codelist.models import KonovaCode, KonovaCodeList
from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount, EcoAccountDeduction
from intervention.models import Legal, Responsibility, Intervention, Handler
from konova.management.commands.setup_data import GROUPS_DATA
from konova.models import Geometry
from konova.models import Geometry, Deadline, DeadlineType
from konova.settings import DEFAULT_GROUP
from konova.utils.generators import generate_random_string
from user.models import UserActionLogEntry
@ -41,6 +41,7 @@ class BaseTestCase(TestCase):
eco_account = None
comp_state = None
comp_action = None
finished_deadline = None
codes = None
superuser_pw = "root"
@ -69,6 +70,7 @@ class BaseTestCase(TestCase):
self.create_dummy_action()
self.codes = self.create_dummy_codes()
self.team = self.create_dummy_team()
self.finished_deadline = self.create_dummy_deadline()
# Set the default group as only group for the user
default_group = self.groups.get(name=DEFAULT_GROUP)
@ -279,6 +281,20 @@ class BaseTestCase(TestCase):
return team
def create_dummy_deadline(self, type: DeadlineType = DeadlineType.FINISHED):
""" Creates a dummy deadline.
If type is not specified, it defaults to DeadlineType.FINISHED
Returns:
deadline (Deadline): A deadline
"""
deadline = Deadline.objects.create(
type=type,
date="1970-01-01"
)
return deadline
@staticmethod
def create_dummy_geometry() -> MultiPolygon:
""" Creates some geometry
@ -361,6 +377,7 @@ class BaseTestCase(TestCase):
compensation.before_states.add(self.comp_state)
compensation.actions.add(self.comp_action)
compensation.geometry.geom = self.create_dummy_geometry()
compensation.deadlines.add(self.finished_deadline)
compensation.geometry.save()
return compensation
@ -390,6 +407,7 @@ class BaseTestCase(TestCase):
ema.before_states.add(self.comp_state)
ema.actions.add(self.comp_action)
ema.geometry.geom = self.create_dummy_geometry()
ema.deadlines.add(self.finished_deadline)
ema.geometry.save()
return ema
@ -410,6 +428,7 @@ class BaseTestCase(TestCase):
eco_account.geometry.geom = self.create_dummy_geometry()
eco_account.geometry.save()
eco_account.deductable_surface = eco_account.get_state_after_surface_sum()
eco_account.deadlines.add(self.finished_deadline)
eco_account.save()
return eco_account

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

@ -207,6 +207,33 @@ class Mailer:
msg
)
def send_mail_deduction_changed_team(self, obj_identifier, obj_title, team, data_changes):
""" Send a mail if deduction has been changed
Args:
obj_identifier (str): Identifier of the main object
obj_title (str): Title of the main object
team (Team): Team to be notified
data_changes (dict): Contains the old|new changes of the deduction changes
Returns:
"""
context = {
"team": team,
"obj_identifier": obj_identifier,
"obj_title": obj_title,
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
"data_changes": data_changes,
}
msg = render_to_string("email/other/deduction_changed_team.html", context)
user_mail_address = team.users.values_list("email", flat=True)
self.send(
user_mail_address,
_("{} - Deduction changed").format(obj_identifier),
msg
)
def send_mail_shared_data_deleted_team(self, obj_identifier, obj_title, team):
""" Send a mail if data has just been deleted
@ -322,6 +349,34 @@ class Mailer:
msg
)
def send_mail_deduction_changed(self, obj_identifier, obj_title, user, data_changes):
""" Send a mail if deduction has been changed
Args:
obj_identifier (str): Identifier of the main object
obj_title (str): Title of the main object
user (User): User to be notified
data_changes (dict): Contains the old|new changes of the deduction changes
Returns:
"""
context = {
"user": user,
"obj_identifier": obj_identifier,
"obj_title": obj_title,
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
"data_changes": data_changes,
}
msg = render_to_string("email/other/deduction_changed.html", context)
user_mail_address = [user.email]
self.send(
user_mail_address,
_("{} - Deduction changed").format(obj_identifier),
msg
)
def send_mail_verify_api_token(self, user):
""" Send a mail if a user creates a new token
@ -343,3 +398,26 @@ class Mailer:
msg
)
def send_mail_resubmission(self, obj_identifier, resubmission):
""" Send a resubmission mail for a user
Args:
obj_identifier (str): The (resubmitted) object's identifier
resubmission (Resubmission): The resubmission
Returns:
"""
context = {
"obj_identifier": obj_identifier,
"resubmission": resubmission,
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
}
msg = render_to_string("email/resubmission/resubmission.html", context)
user_mail_address = [SUPPORT_MAIL_RECIPIENT]
self.send(
user_mail_address,
_("Resubmission - {}").format(obj_identifier),
msg
)

Binary file not shown.

@ -18,15 +18,16 @@
#: konova/filters/mixins.py:277 konova/filters/mixins.py:323
#: konova/filters/mixins.py:361 konova/filters/mixins.py:362
#: konova/filters/mixins.py:393 konova/filters/mixins.py:394
#: konova/forms.py:179 konova/forms.py:281 konova/forms.py:395
#: konova/forms.py:439 konova/forms.py:449 konova/forms.py:462
#: konova/forms.py:474 konova/forms.py:492 user/forms.py:42
#: konova/forms.py:183 konova/forms.py:285 konova/forms.py:399
#: konova/forms.py:443 konova/forms.py:453 konova/forms.py:466
#: konova/forms.py:478 konova/forms.py:496 konova/forms.py:696
#: konova/forms.py:711 user/forms.py:42
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-06-27 14:23+0200\n"
"POT-Creation-Date: 2022-08-15 09:39+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@ -55,7 +56,7 @@ msgstr "Einträge erstellt bis..."
#: analysis/forms.py:49 compensation/forms/forms.py:77
#: compensation/templates/compensation/detail/eco_account/view.html:59
#: compensation/templates/compensation/report/eco_account/report.html:16
#: compensation/utils/quality.py:100 ema/templates/ema/detail/view.html:49
#: compensation/utils/quality.py:111 ema/templates/ema/detail/view.html:49
#: ema/templates/ema/report/report.html:16 ema/utils/quality.py:26
#: intervention/forms/forms.py:102
#: intervention/templates/intervention/detail/view.html:56
@ -85,7 +86,7 @@ msgstr "Bericht generieren"
msgid "Select a timespan and the desired conservation office"
msgstr "Wählen Sie die Zeitspanne und die gewünschte Eintragungsstelle"
#: analysis/forms.py:71 konova/forms.py:227
#: analysis/forms.py:71 konova/forms.py:231
msgid "Continue"
msgstr "Weiter"
@ -241,6 +242,8 @@ msgstr ""
#: ema/templates/ema/detail/includes/states-after.html:36
#: ema/templates/ema/detail/includes/states-before.html:36
#: intervention/forms/modalForms.py:364
#: templates/email/other/deduction_changed.html:31
#: templates/email/other/deduction_changed_team.html:31
msgid "Surface"
msgstr "Fläche"
@ -296,9 +299,9 @@ msgid "Law"
msgstr "Gesetz"
#: analysis/templates/analysis/reports/includes/old_data/amount.html:17
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:28
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:28
#: ema/templates/ema/detail/includes/deadlines.html:28
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:33
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:33
#: ema/templates/ema/detail/includes/deadlines.html:33
msgid "Type"
msgstr "Typ"
@ -307,6 +310,8 @@ msgstr "Typ"
#: intervention/forms/modalForms.py:382 intervention/tables.py:87
#: intervention/templates/intervention/detail/view.html:19
#: konova/templates/konova/includes/quickstart/interventions.html:4
#: templates/email/other/deduction_changed.html:26
#: templates/email/other/deduction_changed_team.html:26
#: templates/navbars/navbar.html:22
msgid "Intervention"
msgstr "Eingriff"
@ -360,7 +365,7 @@ msgstr "Automatisch generiert"
#: intervention/templates/intervention/detail/includes/documents.html:28
#: intervention/templates/intervention/detail/view.html:31
#: intervention/templates/intervention/report/report.html:12
#: konova/forms.py:438
#: konova/forms.py:442
msgid "Title"
msgstr "Bezeichnung"
@ -375,31 +380,32 @@ msgstr "Kompensation XY; Flur ABC"
#: compensation/forms/forms.py:57 compensation/forms/modalForms.py:63
#: compensation/forms/modalForms.py:361 compensation/forms/modalForms.py:469
#: compensation/templates/compensation/detail/compensation/includes/actions.html:35
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:34
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:39
#: compensation/templates/compensation/detail/compensation/includes/documents.html:34
#: compensation/templates/compensation/detail/eco_account/includes/actions.html:34
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:34
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:39
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:34
#: ema/templates/ema/detail/includes/actions.html:34
#: ema/templates/ema/detail/includes/deadlines.html:34
#: ema/templates/ema/detail/includes/deadlines.html:39
#: ema/templates/ema/detail/includes/documents.html:34
#: intervention/forms/forms.py:198 intervention/forms/modalForms.py:175
#: intervention/templates/intervention/detail/includes/documents.html:34
#: intervention/templates/intervention/detail/includes/payments.html:34
#: intervention/templates/intervention/detail/includes/revocation.html:38
#: konova/forms.py:473 konova/templates/konova/includes/comment_card.html:16
#: konova/forms.py:477 konova/forms.py:710
#: konova/templates/konova/includes/comment_card.html:16
msgid "Comment"
msgstr "Kommentar"
#: compensation/forms/forms.py:59 compensation/forms/modalForms.py:471
#: intervention/forms/forms.py:200
#: intervention/forms/forms.py:200 konova/forms.py:712
msgid "Additional comment"
msgstr "Zusätzlicher Kommentar"
#: compensation/forms/forms.py:93
#: compensation/templates/compensation/detail/eco_account/view.html:63
#: compensation/templates/compensation/report/eco_account/report.html:20
#: compensation/utils/quality.py:102 ema/templates/ema/detail/view.html:53
#: compensation/utils/quality.py:113 ema/templates/ema/detail/view.html:53
#: ema/templates/ema/report/report.html:20 ema/utils/quality.py:28
#: intervention/forms/forms.py:130
#: intervention/templates/intervention/detail/view.html:60
@ -477,7 +483,7 @@ msgstr "kompensiert Eingriff"
msgid "Select the intervention for which this compensation compensates"
msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist"
#: compensation/forms/forms.py:219 compensation/views/compensation.py:110
#: compensation/forms/forms.py:219 compensation/views/compensation.py:111
msgid "New compensation"
msgstr "Neue Kompensation"
@ -485,7 +491,7 @@ msgstr "Neue Kompensation"
msgid "Edit compensation"
msgstr "Bearbeite Kompensation"
#: compensation/forms/forms.py:356 compensation/utils/quality.py:84
#: compensation/forms/forms.py:356 compensation/utils/quality.py:95
msgid "Available Surface"
msgstr "Verfügbare Fläche"
@ -495,7 +501,7 @@ msgstr "Die für Abbuchungen zur Verfügung stehende Menge"
#: compensation/forms/forms.py:368
#: compensation/templates/compensation/detail/eco_account/view.html:67
#: compensation/utils/quality.py:72
#: compensation/utils/quality.py:83
msgid "Agreement date"
msgstr "Vereinbarungsdatum"
@ -529,7 +535,7 @@ msgid "Due on which date"
msgstr "Zahlung wird an diesem Datum erwartet"
#: compensation/forms/modalForms.py:65 compensation/forms/modalForms.py:363
#: intervention/forms/modalForms.py:177 konova/forms.py:475
#: intervention/forms/modalForms.py:177 konova/forms.py:479
msgid "Additional comment, maximum {} letters"
msgstr "Zusätzlicher Kommentar, maximal {} Zeichen"
@ -574,7 +580,7 @@ msgstr "Neuer Zustand"
msgid "Insert data for the new state"
msgstr "Geben Sie die Daten des neuen Zustandes ein"
#: compensation/forms/modalForms.py:219 konova/forms.py:229
#: compensation/forms/modalForms.py:219 konova/forms.py:233
msgid "Object removed"
msgstr "Objekt entfernt"
@ -597,10 +603,10 @@ msgid "Select the deadline type"
msgstr "Fristart wählen"
#: compensation/forms/modalForms.py:345
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:31
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:31
#: ema/templates/ema/detail/includes/deadlines.html:31
#: intervention/forms/modalForms.py:149
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:36
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:36
#: ema/templates/ema/detail/includes/deadlines.html:36
#: intervention/forms/modalForms.py:149 konova/forms.py:697
msgid "Date"
msgstr "Datum"
@ -617,9 +623,9 @@ msgid "Insert data for the new deadline"
msgstr "Geben Sie die Daten der neuen Frist ein"
#: compensation/forms/modalForms.py:389
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:59
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:57
#: ema/templates/ema/detail/includes/deadlines.html:57
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:64
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:62
#: ema/templates/ema/detail/includes/deadlines.html:62
msgid "Edit deadline"
msgstr "Frist/Termin bearbeiten"
@ -696,14 +702,14 @@ msgstr ""
msgid "Pieces"
msgstr "Stück"
#: compensation/models/eco_account.py:55
#: compensation/models/eco_account.py:56
msgid ""
"Deductable surface can not be larger than existing surfaces in after states"
msgstr ""
"Die abbuchbare Fläche darf die Gesamtfläche der Zielzustände nicht "
"überschreiten"
#: compensation/models/eco_account.py:62
#: compensation/models/eco_account.py:63
msgid ""
"Deductable surface can not be smaller than the sum of already existing "
"deductions. Please contact the responsible users for the deductions!"
@ -798,18 +804,18 @@ msgid "Amount"
msgstr "Menge"
#: compensation/templates/compensation/detail/compensation/includes/actions.html:40
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:39
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:44
#: compensation/templates/compensation/detail/compensation/includes/documents.html:39
#: compensation/templates/compensation/detail/compensation/includes/states-after.html:41
#: compensation/templates/compensation/detail/compensation/includes/states-before.html:41
#: compensation/templates/compensation/detail/eco_account/includes/actions.html:39
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:38
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:43
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:41
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:38
#: compensation/templates/compensation/detail/eco_account/includes/states-after.html:41
#: compensation/templates/compensation/detail/eco_account/includes/states-before.html:41
#: ema/templates/ema/detail/includes/actions.html:38
#: ema/templates/ema/detail/includes/deadlines.html:38
#: ema/templates/ema/detail/includes/deadlines.html:43
#: ema/templates/ema/detail/includes/documents.html:38
#: ema/templates/ema/detail/includes/states-after.html:40
#: ema/templates/ema/detail/includes/states-before.html:40
@ -848,24 +854,32 @@ msgstr "In LANIS öffnen"
msgid "Public report"
msgstr "Öffentlicher Bericht"
#: compensation/templates/compensation/detail/compensation/includes/controls.html:17
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:31
#: ema/templates/ema/detail/includes/controls.html:31
#: intervention/templates/intervention/detail/includes/controls.html:36
#: compensation/templates/compensation/detail/compensation/includes/controls.html:15
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:15
#: ema/templates/ema/detail/includes/controls.html:15
#: intervention/templates/intervention/detail/includes/controls.html:15
#: konova/forms.py:724 templates/email/resubmission/resubmission.html:4
msgid "Resubmission"
msgstr "Wiedervorlage"
#: compensation/templates/compensation/detail/compensation/includes/controls.html:20
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:34
#: ema/templates/ema/detail/includes/controls.html:34
#: intervention/templates/intervention/detail/includes/controls.html:39
msgid "Edit"
msgstr "Bearbeiten"
#: compensation/templates/compensation/detail/compensation/includes/controls.html:21
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:35
#: ema/templates/ema/detail/includes/controls.html:35
#: intervention/templates/intervention/detail/includes/controls.html:40
msgid "Show log"
msgstr "Log anzeigen"
#: compensation/templates/compensation/detail/compensation/includes/controls.html:24
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:38
#: ema/templates/ema/detail/includes/controls.html:38
#: intervention/templates/intervention/detail/includes/controls.html:43
msgid "Show log"
msgstr "Log anzeigen"
#: compensation/templates/compensation/detail/compensation/includes/controls.html:27
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:41
#: ema/templates/ema/detail/includes/controls.html:41
#: intervention/templates/intervention/detail/includes/controls.html:46
#: venv/lib/python3.7/site-packages/django/forms/formsets.py:391
msgid "Delete"
msgstr "Löschen"
@ -882,9 +896,15 @@ msgstr "Termine und Fristen"
msgid "Add new deadline"
msgstr "Frist/Termin hinzufügen"
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:62
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:60
#: ema/templates/ema/detail/includes/deadlines.html:60
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:25
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:25
#: ema/templates/ema/detail/includes/deadlines.html:25
msgid "Missing finished deadline "
msgstr "Umsetzungstermin fehlt"
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:67
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:65
#: ema/templates/ema/detail/includes/deadlines.html:65
msgid "Remove deadline"
msgstr "Frist löschen"
@ -899,7 +919,7 @@ msgstr "Dokumente"
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:14
#: ema/templates/ema/detail/includes/documents.html:14
#: intervention/templates/intervention/detail/includes/documents.html:14
#: konova/forms.py:491
#: konova/forms.py:495
msgid "Add new document"
msgstr "Neues Dokument hinzufügen"
@ -907,7 +927,7 @@ msgstr "Neues Dokument hinzufügen"
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:31
#: ema/templates/ema/detail/includes/documents.html:31
#: intervention/templates/intervention/detail/includes/documents.html:31
#: konova/forms.py:448
#: konova/forms.py:452
msgid "Created on"
msgstr "Erstellt"
@ -915,7 +935,7 @@ msgstr "Erstellt"
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:61
#: ema/templates/ema/detail/includes/documents.html:61
#: intervention/templates/intervention/detail/includes/documents.html:65
#: konova/forms.py:553
#: konova/forms.py:557
msgid "Edit document"
msgstr "Dokument bearbeiten"
@ -928,7 +948,7 @@ msgstr "Dokument löschen"
#: compensation/templates/compensation/detail/compensation/includes/states-after.html:8
#: compensation/templates/compensation/detail/eco_account/includes/states-after.html:8
#: compensation/utils/quality.py:39
#: compensation/utils/quality.py:40
#: ema/templates/ema/detail/includes/states-after.html:8
msgid "States after"
msgstr "Zielzustand"
@ -974,7 +994,7 @@ msgstr "Zustand entfernen"
#: compensation/templates/compensation/detail/compensation/includes/states-before.html:8
#: compensation/templates/compensation/detail/eco_account/includes/states-before.html:8
#: compensation/utils/quality.py:37
#: compensation/utils/quality.py:38
#: ema/templates/ema/detail/includes/states-before.html:8
msgid "States before"
msgstr "Ausgangszustand"
@ -1067,22 +1087,40 @@ msgstr "Zuletzt bearbeitet"
msgid "Shared with"
msgstr "Freigegeben für"
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:15
#: ema/templates/ema/detail/includes/controls.html:15
#: compensation/templates/compensation/detail/compensation/view.html:132
#: compensation/templates/compensation/detail/eco_account/view.html:110
#: ema/templates/ema/detail/view.html:96
#: intervention/templates/intervention/detail/view.html:138
msgid ""
"The data must be shared with you, if you want to see which other users have "
"shared access as well."
msgstr ""
"Die Daten müssen für Sie freigegeben sein, damit Sie sehen können welche "
"weiteren Nutzern ebenfalls Zugriff hierauf haben."
#: compensation/templates/compensation/detail/compensation/view.html:134
#: compensation/templates/compensation/detail/eco_account/view.html:112
#: ema/templates/ema/detail/view.html:98
#: intervention/templates/intervention/detail/view.html:140
msgid "other users"
msgstr "weitere Nutzer"
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:18
#: ema/templates/ema/detail/includes/controls.html:18
#: intervention/forms/modalForms.py:71
#: intervention/templates/intervention/detail/includes/controls.html:15
#: intervention/templates/intervention/detail/includes/controls.html:18
msgid "Share"
msgstr "Freigabe"
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:20
#: ema/templates/ema/detail/includes/controls.html:20
#: intervention/templates/intervention/detail/includes/controls.html:25
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:23
#: ema/templates/ema/detail/includes/controls.html:23
#: intervention/templates/intervention/detail/includes/controls.html:28
msgid "Unrecord"
msgstr "Entzeichnen"
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:24
#: ema/templates/ema/detail/includes/controls.html:24
#: intervention/templates/intervention/detail/includes/controls.html:29
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:27
#: ema/templates/ema/detail/includes/controls.html:27
#: intervention/templates/intervention/detail/includes/controls.html:32
msgid "Record"
msgstr "Verzeichnen"
@ -1166,48 +1204,57 @@ msgstr "Abbuchungen für"
msgid "None"
msgstr "-"
#: compensation/utils/quality.py:34
#: compensation/utils/quality.py:35
msgid "States unequal"
msgstr "Ungleiche Zustandsflächenmengen"
#: compensation/utils/quality.py:74 intervention/utils/quality.py:84
#: compensation/utils/quality.py:59
msgid "Finished deadlines"
msgstr "Umsetzungstermin"
#: compensation/utils/quality.py:85 intervention/utils/quality.py:84
msgid "Legal data"
msgstr "Rechtliche Daten"
#: compensation/utils/quality.py:88
#: compensation/utils/quality.py:99
msgid "Deductable surface can not be larger than state surface"
msgstr ""
"Die abbuchbare Fläche darf die Gesamtfläche der Zielzustände nicht "
"überschreiten"
#: compensation/utils/quality.py:104 ema/utils/quality.py:30
#: compensation/utils/quality.py:115 ema/utils/quality.py:30
#: intervention/utils/quality.py:55
msgid "Responsible data"
msgstr "Daten zu den verantwortlichen Stellen"
#: compensation/views/compensation.py:53
#: compensation/views/compensation.py:54
msgid "Compensations - Overview"
msgstr "Kompensationen - Übersicht"
#: compensation/views/compensation.py:172 konova/utils/message_templates.py:36
#: compensation/views/compensation.py:173 konova/utils/message_templates.py:36
msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet"
#: compensation/views/compensation.py:182 compensation/views/eco_account.py:173
#: ema/views.py:240 intervention/views.py:338
#: compensation/views/compensation.py:183 compensation/views/eco_account.py:173
#: ema/views.py:241 intervention/views.py:338
msgid "Edit {}"
msgstr "Bearbeite {}"
#: compensation/views/compensation.py:268 compensation/views/eco_account.py:359
#: ema/views.py:194 intervention/views.py:542
#: compensation/views/compensation.py:270 compensation/views/eco_account.py:360
#: ema/views.py:195 intervention/views.py:565
msgid "Log"
msgstr "Log"
#: compensation/views/compensation.py:612 compensation/views/eco_account.py:727
#: ema/views.py:558 intervention/views.py:688
#: compensation/views/compensation.py:614 compensation/views/eco_account.py:728
#: ema/views.py:559 intervention/views.py:711
msgid "Report {}"
msgstr "Bericht {}"
#: compensation/views/compensation.py:680 compensation/views/eco_account.py:862
#: ema/views.py:734 intervention/views.py:496
msgid "Resubmission set"
msgstr "Wiedervorlage gesetzt"
#: compensation/views/eco_account.py:65
msgid "Eco-account - Overview"
msgstr "Ökokonten - Übersicht"
@ -1220,36 +1267,36 @@ msgstr "Ökokonto {} hinzugefügt"
msgid "Eco-Account {} edited"
msgstr "Ökokonto {} bearbeitet"
#: compensation/views/eco_account.py:276
#: compensation/views/eco_account.py:277
msgid "Eco-account removed"
msgstr "Ökokonto entfernt"
#: compensation/views/eco_account.py:380 ema/views.py:282
#: intervention/views.py:641
#: compensation/views/eco_account.py:381 ema/views.py:283
#: intervention/views.py:664
msgid "{} unrecorded"
msgstr "{} entzeichnet"
#: compensation/views/eco_account.py:380 ema/views.py:282
#: intervention/views.py:641
#: compensation/views/eco_account.py:381 ema/views.py:283
#: intervention/views.py:664
msgid "{} recorded"
msgstr "{} verzeichnet"
#: compensation/views/eco_account.py:804 ema/views.py:628
#: compensation/views/eco_account.py:805 ema/views.py:629
#: intervention/views.py:439
msgid "{} has already been shared with you"
msgstr "{} wurde bereits für Sie freigegeben"
#: compensation/views/eco_account.py:809 ema/views.py:633
#: compensation/views/eco_account.py:810 ema/views.py:634
#: intervention/views.py:444
msgid "{} has been shared with you"
msgstr "{} ist nun für Sie freigegeben"
#: compensation/views/eco_account.py:816 ema/views.py:640
#: compensation/views/eco_account.py:817 ema/views.py:641
#: intervention/views.py:451
msgid "Share link invalid"
msgstr "Freigabelink ungültig"
#: compensation/views/eco_account.py:839 ema/views.py:663
#: compensation/views/eco_account.py:840 ema/views.py:664
#: intervention/views.py:474
msgid "Share settings updated"
msgstr "Freigabe Einstellungen aktualisiert"
@ -1290,11 +1337,11 @@ msgstr "EMAs - Übersicht"
msgid "EMA {} added"
msgstr "EMA {} hinzugefügt"
#: ema/views.py:230
#: ema/views.py:231
msgid "EMA {} edited"
msgstr "EMA {} bearbeitet"
#: ema/views.py:263
#: ema/views.py:264
msgid "EMA removed"
msgstr "EMA entfernt"
@ -1432,11 +1479,11 @@ msgid "Checked compensations data and payments"
msgstr "Kompensationen und Zahlungen geprüft"
#: intervention/forms/modalForms.py:263
#: intervention/templates/intervention/detail/includes/controls.html:19
#: intervention/templates/intervention/detail/includes/controls.html:22
msgid "Run check"
msgstr "Prüfung vornehmen"
#: intervention/forms/modalForms.py:264 konova/forms.py:594
#: intervention/forms/modalForms.py:264 konova/forms.py:598
msgid ""
"I, {} {}, confirm that all necessary control steps have been performed by "
"myself."
@ -1549,18 +1596,6 @@ msgstr "Eingriffsverursacher"
msgid "Exists"
msgstr "vorhanden"
#: intervention/templates/intervention/detail/view.html:138
msgid ""
"The data must be shared with you, if you want to see which other users have "
"shared access as well."
msgstr ""
"Die Daten müssen für Sie freigegeben sein, damit Sie sehen können welche weiteren Nutzern "
"ebenfalls Zugriff hierauf haben."
#: intervention/templates/intervention/detail/view.html:140
msgid "other users"
msgstr "weitere Nutzer"
#: intervention/templates/intervention/report/report.html:58
msgid "Deductions of eco-accounts"
msgstr "Abbuchungen von Ökokonten"
@ -1604,11 +1639,11 @@ msgstr "Eingriff {} bearbeitet"
msgid "{} removed"
msgstr "{} entfernt"
#: intervention/views.py:495
#: intervention/views.py:518
msgid "Check performed"
msgstr "Prüfung durchgeführt"
#: intervention/views.py:646
#: intervention/views.py:669
msgid "There are errors on this intervention:"
msgstr "Es liegen Fehler in diesem Eingriff vor:"
@ -1693,78 +1728,90 @@ msgstr "Nach Zulassungsbehörde suchen"
msgid "Search for conservation office"
msgstr "Nch Eintragungsstelle suchen"
#: konova/forms.py:41 templates/form/collapsable/form.html:62
#: konova/forms.py:44 templates/form/collapsable/form.html:62
msgid "Save"
msgstr "Speichern"
#: konova/forms.py:75
#: konova/forms.py:78
msgid "Not editable"
msgstr "Nicht editierbar"
#: konova/forms.py:178 konova/forms.py:394
#: konova/forms.py:182 konova/forms.py:398
msgid "Confirm"
msgstr "Bestätige"
#: konova/forms.py:190 konova/forms.py:403
#: konova/forms.py:194 konova/forms.py:407
msgid "Remove"
msgstr "Löschen"
#: konova/forms.py:192
#: konova/forms.py:196
msgid "You are about to remove {} {}"
msgstr "Sie sind dabei {} {} zu löschen"
#: konova/forms.py:280 konova/utils/quality.py:44 konova/utils/quality.py:46
#: konova/forms.py:284 konova/utils/quality.py:44 konova/utils/quality.py:46
#: templates/form/collapsable/form.html:45
msgid "Geometry"
msgstr "Geometrie"
#: konova/forms.py:331
#: konova/forms.py:335
msgid "Only surfaces allowed. Points or lines must be buffered."
msgstr ""
"Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden."
#: konova/forms.py:404
#: konova/forms.py:408
msgid "Are you sure?"
msgstr "Sind Sie sicher?"
#: konova/forms.py:450
#: konova/forms.py:454
msgid "When has this file been created? Important for photos."
msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?"
#: konova/forms.py:461
#: konova/forms.py:465
#: venv/lib/python3.7/site-packages/django/db/models/fields/files.py:231
msgid "File"
msgstr "Datei"
#: konova/forms.py:463
#: konova/forms.py:467
msgid "Allowed formats: pdf, jpg, png. Max size 15 MB."
msgstr "Formate: pdf, jpg, png. Maximal 15 MB."
#: konova/forms.py:528
#: konova/forms.py:532
msgid "Added document"
msgstr "Dokument hinzugefügt"
#: konova/forms.py:585
#: konova/forms.py:589
msgid "Confirm record"
msgstr "Verzeichnen bestätigen"
#: konova/forms.py:593
#: konova/forms.py:597
msgid "Record data"
msgstr "Daten verzeichnen"
#: konova/forms.py:600
#: konova/forms.py:604
msgid "Confirm unrecord"
msgstr "Entzeichnen bestätigen"
#: konova/forms.py:601
#: konova/forms.py:605
msgid "Unrecord data"
msgstr "Daten entzeichnen"
#: konova/forms.py:602
#: konova/forms.py:606
msgid "I, {} {}, confirm that this data must be unrecorded."
msgstr ""
"Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen."
#: konova/forms.py:698
msgid "When do you want to be reminded?"
msgstr "Wann wollen Sie erinnert werden?"
#: konova/forms.py:725
msgid "Set your resubmission for this entry."
msgstr "Setzen Sie eine Wiedervorlage für diesen Eintrag."
#: konova/forms.py:746
msgid "The date should be in the future"
msgstr "Das Datum sollte in der Zukunft liegen"
#: konova/management/commands/setup_data.py:26
msgid "On shared access gained"
msgstr "Wenn mir eine Freigabe zu Daten erteilt wird"
@ -1785,6 +1832,10 @@ msgstr "Wenn meine freigegebenen Daten gelöscht wurden"
msgid "On shared data checked"
msgstr "Wenn meine freigegebenen Daten geprüft wurden"
#: konova/management/commands/setup_data.py:31
msgid "On deduction changes"
msgstr "Wenn eine Abbuchung zu meinem Ökokonto verändert wird"
#: konova/models/deadline.py:18
msgid "Finished"
msgstr "Umgesetzt bis"
@ -1895,26 +1946,34 @@ msgstr "{} - Zugriff entzogen"
msgid "{} - Shared access given"
msgstr "{} - Zugriff freigegeben"
#: konova/utils/mailer.py:160 konova/utils/mailer.py:275
#: konova/utils/mailer.py:160 konova/utils/mailer.py:302
msgid "{} - Shared data unrecorded"
msgstr "{} - Freigegebene Daten entzeichnet"
#: konova/utils/mailer.py:183 konova/utils/mailer.py:252
#: konova/utils/mailer.py:183 konova/utils/mailer.py:279
msgid "{} - Shared data recorded"
msgstr "{} - Freigegebene Daten verzeichnet"
#: konova/utils/mailer.py:206 konova/utils/mailer.py:321
#: konova/utils/mailer.py:206 konova/utils/mailer.py:348
msgid "{} - Shared data checked"
msgstr "{} - Freigegebene Daten geprüft"
#: konova/utils/mailer.py:229 konova/utils/mailer.py:298
#: konova/utils/mailer.py:233 konova/utils/mailer.py:376
msgid "{} - Deduction changed"
msgstr "{} - Abbuchung geändert"
#: konova/utils/mailer.py:256 konova/utils/mailer.py:325
msgid "{} - Shared data deleted"
msgstr "{} - Freigegebene Daten gelöscht"
#: konova/utils/mailer.py:342 templates/email/api/verify_token.html:4
#: konova/utils/mailer.py:397 templates/email/api/verify_token.html:4
msgid "Request for new API token"
msgstr "Anfrage für neuen API Token"
#: konova/utils/mailer.py:420
msgid "Resubmission - {}"
msgstr "Wiedervorlage - {}"
#: konova/utils/message_templates.py:10
msgid "no further details"
msgstr "keine weitere Angabe"
@ -2197,11 +2256,11 @@ msgstr "Irgendetwas ist passiert. Wir arbeiten daran!"
msgid "Hello support"
msgstr "Hallo Support"
#: templates/email/api/verify_token.html:9
#: templates/email/api/verify_token.html:10
msgid "you need to verify the API token for user"
msgstr "Sie müssen einen API Token für folgenden Nutzer freischalten"
#: templates/email/api/verify_token.html:15
#: templates/email/api/verify_token.html:16
msgid ""
"If unsure, please contact the user. The API token can not be used until you "
"activated it in the admin backend."
@ -2210,19 +2269,22 @@ msgstr ""
"Token kann so lange nicht verwendet werden, wie er noch nicht von Ihnen im "
"Admin Backend aktiviert worden ist."
#: templates/email/api/verify_token.html:18
#: templates/email/checking/shared_data_checked.html:19
#: templates/email/checking/shared_data_checked_team.html:19
#: templates/email/deleting/shared_data_deleted.html:19
#: templates/email/deleting/shared_data_deleted_team.html:19
#: templates/email/recording/shared_data_recorded.html:19
#: templates/email/recording/shared_data_recorded_team.html:19
#: templates/email/recording/shared_data_unrecorded.html:19
#: templates/email/recording/shared_data_unrecorded_team.html:19
#: templates/email/sharing/shared_access_given.html:20
#: templates/email/sharing/shared_access_given_team.html:20
#: templates/email/sharing/shared_access_removed.html:20
#: templates/email/sharing/shared_access_removed_team.html:20
#: templates/email/api/verify_token.html:19
#: templates/email/checking/shared_data_checked.html:20
#: templates/email/checking/shared_data_checked_team.html:20
#: templates/email/deleting/shared_data_deleted.html:20
#: templates/email/deleting/shared_data_deleted_team.html:20
#: templates/email/other/deduction_changed.html:41
#: templates/email/other/deduction_changed_team.html:41
#: templates/email/recording/shared_data_recorded.html:20
#: templates/email/recording/shared_data_recorded_team.html:20
#: templates/email/recording/shared_data_unrecorded.html:20
#: templates/email/recording/shared_data_unrecorded_team.html:20
#: templates/email/resubmission/resubmission.html:21
#: templates/email/sharing/shared_access_given.html:21
#: templates/email/sharing/shared_access_given_team.html:21
#: templates/email/sharing/shared_access_removed.html:21
#: templates/email/sharing/shared_access_removed_team.html:21
msgid "Best regards"
msgstr "Beste Grüße"
@ -2233,20 +2295,22 @@ msgstr "Freigegebene Daten geprüft"
#: templates/email/checking/shared_data_checked.html:8
#: templates/email/deleting/shared_data_deleted.html:8
#: templates/email/other/deduction_changed.html:8
#: templates/email/recording/shared_data_recorded.html:8
#: templates/email/recording/shared_data_unrecorded.html:8
#: templates/email/resubmission/resubmission.html:8
#: templates/email/sharing/shared_access_given.html:8
#: templates/email/sharing/shared_access_removed.html:8
msgid "Hello "
msgstr "Hallo "
#: templates/email/checking/shared_data_checked.html:10
#: templates/email/checking/shared_data_checked_team.html:10
#: templates/email/checking/shared_data_checked.html:11
#: templates/email/checking/shared_data_checked_team.html:11
msgid "the following dataset has just been checked"
msgstr "der folgende Datensatz wurde soeben geprüft "
#: templates/email/checking/shared_data_checked.html:16
#: templates/email/checking/shared_data_checked_team.html:16
#: templates/email/checking/shared_data_checked.html:17
#: templates/email/checking/shared_data_checked_team.html:17
msgid ""
"This means, the responsible registration office just confirmed the "
"correctness of this dataset."
@ -2256,6 +2320,7 @@ msgstr ""
#: templates/email/checking/shared_data_checked_team.html:8
#: templates/email/deleting/shared_data_deleted_team.html:8
#: templates/email/other/deduction_changed_team.html:8
#: templates/email/recording/shared_data_recorded_team.html:8
#: templates/email/recording/shared_data_unrecorded_team.html:8
#: templates/email/sharing/shared_access_given_team.html:8
@ -2268,13 +2333,15 @@ msgstr "Hallo Team"
msgid "Shared data deleted"
msgstr "Freigegebene Daten gelöscht"
#: templates/email/deleting/shared_data_deleted.html:10
#: templates/email/deleting/shared_data_deleted_team.html:10
#: templates/email/deleting/shared_data_deleted.html:11
#: templates/email/deleting/shared_data_deleted_team.html:11
msgid "the following dataset has just been deleted"
msgstr "der folgende Datensatz wurde soeben gelöscht "
#: templates/email/deleting/shared_data_deleted.html:16
#: templates/email/deleting/shared_data_deleted_team.html:16
#: templates/email/deleting/shared_data_deleted.html:17
#: templates/email/deleting/shared_data_deleted_team.html:17
#: templates/email/other/deduction_changed.html:38
#: templates/email/other/deduction_changed_team.html:38
msgid ""
"If this should not have been happened, please contact us. See the signature "
"for details."
@ -2282,24 +2349,55 @@ msgstr ""
"Falls das nicht hätte passieren dürfen, kontaktieren Sie uns bitte. In der E-"
"mail Signatur finden Sie weitere Kontaktinformationen."
#: templates/email/other/deduction_changed.html:4
#: templates/email/other/deduction_changed_team.html:4
msgid "Deduction changed"
msgstr "Abbuchung geändert"
#: templates/email/other/deduction_changed.html:11
#: templates/email/other/deduction_changed_team.html:11
msgid "a deduction of this eco account has changed:"
msgstr "eine Abbuchung des Ökokontos hat sich geändert:"
#: templates/email/other/deduction_changed.html:16
#: templates/email/other/deduction_changed_team.html:16
msgid "Attribute"
msgstr "Attribute"
#: templates/email/other/deduction_changed.html:17
#: templates/email/other/deduction_changed_team.html:17
msgid "Old"
msgstr "Alt"
#: templates/email/other/deduction_changed.html:18
#: templates/email/other/deduction_changed_team.html:18
#: templates/generic_index.html:43 user/templates/user/team/index.html:22
msgid "New"
msgstr "Neu"
#: templates/email/other/deduction_changed.html:21
#: templates/email/other/deduction_changed_team.html:21
msgid "EcoAccount"
msgstr "Ökokonto"
#: templates/email/recording/shared_data_recorded.html:4
#: templates/email/recording/shared_data_recorded_team.html:4
msgid "Shared data recorded"
msgstr "Freigegebene Daten verzeichnet"
#: templates/email/recording/shared_data_recorded.html:10
#: templates/email/recording/shared_data_recorded_team.html:10
#: templates/email/recording/shared_data_recorded.html:11
#: templates/email/recording/shared_data_recorded_team.html:11
msgid "the following dataset has just been recorded"
msgstr "der folgende Datensatz wurde soeben verzeichnet "
#: templates/email/recording/shared_data_recorded.html:16
#: templates/email/recording/shared_data_recorded_team.html:16
#: templates/email/recording/shared_data_recorded.html:17
#: templates/email/recording/shared_data_recorded_team.html:17
msgid "This means the data is now publicly available, e.g. in LANIS"
msgstr ""
"Das bedeutet, dass die Daten nun öffentlich verfügbar sind, z.B. im LANIS."
#: templates/email/recording/shared_data_recorded.html:26
#: templates/email/recording/shared_data_recorded_team.html:26
#: templates/email/recording/shared_data_recorded.html:27
#: templates/email/recording/shared_data_recorded_team.html:27
msgid ""
"Please note: Recorded intervention means the compensations are recorded as "
"well."
@ -2312,18 +2410,18 @@ msgstr ""
msgid "Shared data unrecorded"
msgstr "Freigegebene Daten entzeichnet"
#: templates/email/recording/shared_data_unrecorded.html:10
#: templates/email/recording/shared_data_unrecorded_team.html:10
#: templates/email/recording/shared_data_unrecorded.html:11
#: templates/email/recording/shared_data_unrecorded_team.html:11
msgid "the following dataset has just been unrecorded"
msgstr "der folgende Datensatz wurde soeben entzeichnet "
#: templates/email/recording/shared_data_unrecorded.html:16
#: templates/email/recording/shared_data_unrecorded_team.html:16
#: templates/email/recording/shared_data_unrecorded.html:17
#: templates/email/recording/shared_data_unrecorded_team.html:17
msgid "This means the data is no longer publicly available."
msgstr "Das bedeutet, dass die Daten nicht länger öffentlich verfügbar sind."
#: templates/email/recording/shared_data_unrecorded.html:26
#: templates/email/recording/shared_data_unrecorded_team.html:26
#: templates/email/recording/shared_data_unrecorded.html:27
#: templates/email/recording/shared_data_unrecorded_team.html:27
msgid ""
"Please note: Unrecorded intervention means the compensations are unrecorded "
"as well."
@ -2331,22 +2429,30 @@ msgstr ""
"Bitte beachten Sie: Entzeichnete Eingriffe bedeuten, dass auch die "
"zugehörigen Kompensationen automatisch entzeichnet worden sind."
#: templates/email/resubmission/resubmission.html:11
msgid "you wanted to be reminded on this entry."
msgstr "Sie wollten an diesen Eintrag erinnert werden."
#: templates/email/resubmission/resubmission.html:15
msgid "Your personal comment:"
msgstr "Ihr Kommentar:"
#: templates/email/sharing/shared_access_given.html:4
#: templates/email/sharing/shared_access_given_team.html:4
msgid "Access shared"
msgstr "Zugriff freigegeben"
#: templates/email/sharing/shared_access_given.html:10
#: templates/email/sharing/shared_access_given.html:11
msgid "the following dataset has just been shared with you"
msgstr "der folgende Datensatz wurde soeben für Sie freigegeben "
#: templates/email/sharing/shared_access_given.html:16
#: templates/email/sharing/shared_access_given_team.html:16
#: templates/email/sharing/shared_access_given.html:17
#: templates/email/sharing/shared_access_given_team.html:17
msgid "This means you can now edit this dataset."
msgstr "Das bedeutet, dass Sie diesen Datensatz nun auch bearbeiten können."
#: templates/email/sharing/shared_access_given.html:17
#: templates/email/sharing/shared_access_given_team.html:17
#: templates/email/sharing/shared_access_given.html:18
#: templates/email/sharing/shared_access_given_team.html:18
msgid ""
"The shared dataset appears now by default on your overview for this dataset "
"type."
@ -2354,8 +2460,8 @@ msgstr ""
"Der freigegebene Datensatz ist nun standardmäßig in Ihrer Übersicht für den "
"Datensatztyp im KSP gelistet."
#: templates/email/sharing/shared_access_given.html:27
#: templates/email/sharing/shared_access_given_team.html:27
#: templates/email/sharing/shared_access_given.html:28
#: templates/email/sharing/shared_access_given_team.html:28
msgid ""
"Please note: Shared access on an intervention means you automatically have "
"editing access to related compensations."
@ -2364,7 +2470,7 @@ msgstr ""
"Sie automatisch auch Zugriff auf die zugehörigen Kompensationen erhalten "
"haben."
#: templates/email/sharing/shared_access_given_team.html:10
#: templates/email/sharing/shared_access_given_team.html:11
msgid "the following dataset has just been shared with your team"
msgstr "der folgende Datensatz wurde soeben für Ihr Team freigegeben "
@ -2373,20 +2479,20 @@ msgstr "der folgende Datensatz wurde soeben für Ihr Team freigegeben "
msgid "Shared access removed"
msgstr "Freigegebener Zugriff entzogen"
#: templates/email/sharing/shared_access_removed.html:10
#: templates/email/sharing/shared_access_removed.html:11
msgid ""
"your shared access, including editing, has been revoked for the dataset "
msgstr ""
"Ihnen wurde soeben der bearbeitende Zugriff auf den folgenden Datensatz "
"entzogen: "
#: templates/email/sharing/shared_access_removed.html:16
#: templates/email/sharing/shared_access_removed_team.html:16
#: templates/email/sharing/shared_access_removed.html:17
#: templates/email/sharing/shared_access_removed_team.html:17
msgid "However, you are still able to view the dataset content."
msgstr "Sie können den Datensatz aber immer noch im KSP einsehen."
#: templates/email/sharing/shared_access_removed.html:17
#: templates/email/sharing/shared_access_removed_team.html:17
#: templates/email/sharing/shared_access_removed.html:18
#: templates/email/sharing/shared_access_removed_team.html:18
msgid ""
"Please use the provided search filter on the dataset`s overview pages to "
"find them."
@ -2394,7 +2500,7 @@ msgstr ""
"Nutzen Sie hierzu einfach die entsprechenden Suchfilter auf den "
"Übersichtsseiten"
#: templates/email/sharing/shared_access_removed_team.html:10
#: templates/email/sharing/shared_access_removed_team.html:11
msgid ""
"your teams shared access, including editing, has been revoked for the "
"dataset "
@ -2479,10 +2585,6 @@ msgstr "* sind Pflichtfelder."
msgid "New entry"
msgstr "Neuer Eintrag"
#: templates/generic_index.html:43 user/templates/user/team/index.html:22
msgid "New"
msgstr "Neu"
#: templates/generic_index.html:58
msgid "Search for keywords"
msgstr "Nach Schlagwörtern suchen"
@ -2753,7 +2855,7 @@ msgstr "Benachrichtigungen"
msgid "Manage teams"
msgstr ""
#: user/templates/user/index.html:61 user/templates/user/team/index.html:18
#: user/templates/user/index.html:61 user/templates/user/team/index.html:19
#: user/views.py:167
msgid "Teams"
msgstr ""

@ -6,6 +6,7 @@
<article>
{% trans 'Hello support' %},
<br>
<br>
{% trans 'you need to verify the API token for user' %}:
<br>
<br>

@ -7,6 +7,7 @@
<article>
{% trans 'Hello ' %} {{user.username}},
<br>
<br>
{% trans 'the following dataset has just been checked' %}
<br>
<strong>{{obj_identifier}}</strong>

@ -7,6 +7,7 @@
<article>
{% trans 'Hello team' %} {{team.name}},
<br>
<br>
{% trans 'the following dataset has just been checked' %}
<br>
<strong>{{obj_identifier}}</strong>

@ -7,6 +7,7 @@
<article>
{% trans 'Hello ' %} {{user.username}},
<br>
<br>
{% trans 'the following dataset has just been deleted' %}
<br>
<strong>{{obj_identifier}}</strong>

@ -7,6 +7,7 @@
<article>
{% trans 'Hello team' %} {{team.name}},
<br>
<br>
{% trans 'the following dataset has just been deleted' %}
<br>
<strong>{{obj_identifier}}</strong>

@ -0,0 +1,50 @@
{% load i18n %}
<div>
<h2>{% translate 'Deduction changed' %}</h2>
<h4>{{obj_identifier}}</h4>
<hr>
<article>
{% translate 'Hello ' %} {{user.username}},
<br>
<br>
{% translate 'a deduction of this eco account has changed:' %}
<br>
<br>
<table>
<tr>
<th scope="col">{% translate 'Attribute' %}</th>
<th scope="col">{% translate 'Old' %}</th>
<th scope="col">{% translate 'New' %}</th>
</tr>
<tr>
<td>{% translate 'EcoAccount' %}</td>
<td>{{data_changes.account.old}}</td>
<td>{{data_changes.account.new}}</td>
</tr>
<tr>
<td>{% translate 'Intervention' %}</td>
<td>{{data_changes.intervention.old}}</td>
<td>{{data_changes.intervention.new}}</td>
</tr>
<tr>
<td>{% translate 'Surface' %}</td>
<td>{{data_changes.surface.old}} m²</td>
<td>{{data_changes.surface.new}} m²</td>
</tr>
</table>
<br>
<br>
{% translate 'If this should not have been happened, please contact us. See the signature for details.' %}
<br>
<br>
{% translate 'Best regards' %}
<br>
KSP
<br>
<br>
<br>
{% include 'email/signature.html' %}
</article>
</div>

@ -0,0 +1,50 @@
{% load i18n %}
<div>
<h2>{% translate 'Deduction changed' %}</h2>
<h4>{{obj_identifier}}</h4>
<hr>
<article>
{% trans 'Hello team' %} {{team.name}},
<br>
<br>
{% translate 'a deduction of this eco account has changed:' %}
<br>
<br>
<table>
<tr>
<th scope="col">{% translate 'Attribute' %}</th>
<th scope="col">{% translate 'Old' %}</th>
<th scope="col">{% translate 'New' %}</th>
</tr>
<tr>
<td>{% translate 'EcoAccount' %}</td>
<td>{{data_changes.account.old}}</td>
<td>{{data_changes.account.new}}</td>
</tr>
<tr>
<td>{% translate 'Intervention' %}</td>
<td>{{data_changes.intervention.old}}</td>
<td>{{data_changes.intervention.new}}</td>
</tr>
<tr>
<td>{% translate 'Surface' %}</td>
<td>{{data_changes.surface.old}} m²</td>
<td>{{data_changes.surface.new}} m²</td>
</tr>
</table>
<br>
<br>
{% translate 'If this should not have been happened, please contact us. See the signature for details.' %}
<br>
<br>
{% translate 'Best regards' %}
<br>
KSP
<br>
<br>
<br>
{% include 'email/signature.html' %}
</article>
</div>

@ -7,6 +7,7 @@
<article>
{% trans 'Hello ' %} {{user.username}},
<br>
<br>
{% trans 'the following dataset has just been recorded' %}
<br>
<strong>{{obj_identifier}}</strong>

@ -7,6 +7,7 @@
<article>
{% trans 'Hello team' %} {{team.name}},
<br>
<br>
{% trans 'the following dataset has just been recorded' %}
<br>
<strong>{{obj_identifier}}</strong>

@ -7,6 +7,7 @@
<article>
{% trans 'Hello ' %} {{user.username}},
<br>
<br>
{% trans 'the following dataset has just been unrecorded' %}
<br>
<strong>{{obj_identifier}}</strong>

@ -7,6 +7,7 @@
<article>
{% trans 'Hello team' %} {{team.name}},
<br>
<br>
{% trans 'the following dataset has just been unrecorded' %}
<br>
<strong>{{obj_identifier}}</strong>

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

@ -7,6 +7,7 @@
<article>
{% trans 'Hello ' %} {{user.username}},
<br>
<br>
{% trans 'the following dataset has just been shared with you' %}
<br>
<strong>{{obj_identifier}}</strong>

@ -7,6 +7,7 @@
<article>
{% trans 'Hello team' %} {{team.name}},
<br>
<br>
{% trans 'the following dataset has just been shared with your team' %}
<br>
<strong>{{obj_identifier}}</strong>

@ -7,6 +7,7 @@
<article>
{% trans 'Hello ' %} {{user.username}},
<br>
<br>
{% trans 'your shared access, including editing, has been revoked for the dataset ' %}
<br>
<strong>{{obj_identifier}}</strong>

@ -7,6 +7,7 @@
<article>
{% trans 'Hello team' %} {{team.name}},
<br>
<br>
{% trans 'your teams shared access, including editing, has been revoked for the dataset ' %}
<br>
<strong>{{obj_identifier}}</strong>

@ -13,4 +13,5 @@ class UserNotificationEnum(BaseEnum):
NOTIFY_ON_SHARED_DATA_RECORDED = "NOTIFY_ON_SHARED_DATA_RECORDED" # notifies in case data has been "verzeichnet"
NOTIFY_ON_SHARED_DATA_DELETED = "NOTIFY_ON_SHARED_DATA_DELETED" # notifies in case data has been deleted
NOTIFY_ON_SHARED_DATA_CHECKED = "NOTIFY_ON_SHARED_DATA_CHECKED" # notifies in case shared data has been checked
NOTIFY_ON_SHARED_ACCESS_GAINED = "NOTIFY_ON_SHARED_ACCESS_GAINED" # notifies in case new access has been gained
NOTIFY_ON_SHARED_ACCESS_GAINED = "NOTIFY_ON_SHARED_ACCESS_GAINED" # notifies in case new access has been gained
NOTIFY_ON_DEDUCTION_CHANGES = "NOTIFY_ON_DEDUCTION_CHANGES" # notifies in case any changes (edit|remove) have been performed on a deduction of the user's ecoaccounts

@ -7,7 +7,7 @@ Created on: 08.07.21
"""
from dal import autocomplete
from django import forms
from django.db import IntegrityError, transaction
from django.db import transaction
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
@ -15,7 +15,8 @@ from api.models import APIUserToken
from intervention.inputs import GenerateInput
from user.models import User, UserNotification, Team
from konova.forms import BaseForm, BaseModalForm, RemoveModalForm
from konova.forms.modals import BaseModalForm, RemoveModalForm
from konova.forms import BaseForm
class UserNotificationForm(BaseForm):

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

@ -95,6 +95,20 @@ class Team(UuidModel, DeletableObjectMixin):
mailer = Mailer()
mailer.send_mail_shared_data_checked_team(obj_identifier, obj_title, self)
def send_mail_deduction_changed(self, obj_identifier, obj_title, data_changes):
""" Sends a mail to the team members in case of changed deduction values
Args:
obj_identifier (str): Identifier of the main object
obj_title (str): Title of the main object
data_changes (dict): Contains the old|new changes of the deduction changes
Returns:
"""
mailer = Mailer()
mailer.send_mail_deduction_changed_team(obj_identifier, obj_title, self, data_changes)
def send_mail_shared_data_deleted(self, obj_identifier, obj_title):
""" Sends a mail to the team members in case of deleted data

@ -145,6 +145,22 @@ class User(AbstractUser):
mailer = Mailer()
mailer.send_mail_shared_data_checked(obj_identifier, obj_title, self)
def send_mail_deduction_changed(self, obj_identifier, obj_title, data_changes):
""" Sends a mail to the user in case of a changed deduction
Args:
obj_identifier (str): Identifier of the main object
obj_title (str): Title of the main object
data_changes (dict): Contains the old|new changes of the deduction changes
Returns:
"""
notification_set = self.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_DEDUCTION_CHANGES)
if notification_set:
mailer = Mailer()
mailer.send_mail_deduction_changed(obj_identifier, obj_title, self, data_changes)
def get_API_token(self):
""" Getter for an API token

Loading…
Cancel
Save