test #347

Merged
mpeltriaux merged 22 commits from test into master 1 year ago

4
.gitignore vendored

@ -1,3 +1,5 @@
# Project exclude paths # Project exclude paths
/venv/ /venv/
/.idea/ /.idea/
/.coverage
/htmlcov/

@ -9,4 +9,8 @@ Created on: 19.10.21
# Defines the date of the legal publishing of the LKompVzVo # Defines the date of the legal publishing of the LKompVzVo
from django.utils import timezone from django.utils import timezone
LKOMPVZVO_PUBLISH_DATE = timezone.make_aware(timezone.datetime.fromisoformat("2018-06-16")).date() LKOMPVZVO_PUBLISH_DATE = timezone.make_aware(
timezone.datetime.fromisoformat(
"2018-06-16"
)
).date()

@ -31,6 +31,6 @@
{% include 'analysis/reports/includes/intervention/card_intervention.html' %} {% include 'analysis/reports/includes/intervention/card_intervention.html' %}
{% include 'analysis/reports/includes/compensation/card_compensation.html' %} {% include 'analysis/reports/includes/compensation/card_compensation.html' %}
{% include 'analysis/reports/includes/eco_account/card_eco_account.html' %} {% include 'analysis/reports/includes/eco_account/card_eco_account.html' %}
{% include 'analysis/reports/includes/old_data/card_old_interventions.html' %} {% include 'analysis/reports/includes/old_data/card_old_data.html' %}
</div> </div>
{% endblock %} {% endblock %}

@ -10,6 +10,7 @@
{% fa5_icon 'leaf' %} {% fa5_icon 'leaf' %}
{% trans 'Compensations' %} {% trans 'Compensations' %}
</h5> </h5>
<span>{% trans 'Binding date after' %} 16.06.2018</span>
</div> </div>
</div> </div>
</div> </div>

@ -10,6 +10,7 @@
{% fa5_icon 'tree' %} {% fa5_icon 'tree' %}
{% trans 'Eco-Accounts' %} {% trans 'Eco-Accounts' %}
</h5> </h5>
<span>{% trans 'Binding date after' %} 16.06.2018</span>
</div> </div>
</div> </div>
</div> </div>

@ -9,6 +9,7 @@
{% fa5_icon 'pencil-ruler' %} {% fa5_icon 'pencil-ruler' %}
{% trans 'Interventions' %} {% trans 'Interventions' %}
</h5> </h5>
<span>{% trans 'Binding date after' %} 16.06.2018</span>
</div> </div>
</div> </div>
</div> </div>

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 15.08.23
"""

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 15.08.23
"""

@ -0,0 +1,47 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 15.08.23
"""
from datetime import timedelta
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from analysis.forms import TimespanReportForm
from konova.tests.test_views import BaseTestCase
class TimeSpanReportFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
eiv = self.create_dummy_intervention()
def test_init(self):
form = TimespanReportForm()
self.assertEqual(form.form_title, str(_("Generate report")))
self.assertEqual(form.form_caption, str(_("Select a timespan and the desired conservation office") ))
self.assertEqual(form.action_url, reverse("analysis:reports"))
self.assertFalse(form.show_cancel_btn)
self.assertEqual(form.action_btn_label, str(_("Continue")))
def test_save(self):
date_from = now().date() - timedelta(days=365)
date_to = now().date()
office = self.get_conservation_office_code()
data = {
"date_from": date_from,
"date_to": date_to,
"conservation_office": office,
}
form = TimespanReportForm(data)
self.assertTrue(form.is_valid(), msg=f"{form.errors}")
detail_report_url = form.save()
self.assertEqual(
detail_report_url,
reverse("analysis:report-detail", args=(office.id,)) + f"?df={date_from}&dt={date_to}"
)

@ -0,0 +1,98 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 17.08.23
"""
from datetime import timedelta
from django.utils.timezone import now
from analysis.settings import LKOMPVZVO_PUBLISH_DATE
from analysis.utils.report import TimespanReport
from konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT
from konova.tests.test_views import BaseTestCase
class TimeSpanReportTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
today = now().date()
old_date = LKOMPVZVO_PUBLISH_DATE - timedelta(days=1)
self.conservation_office = self.get_conservation_office_code()
self.eiv_old = self.create_dummy_intervention()
self.kom_old = self.create_dummy_compensation(interv=self.eiv_old)
self.assertNotEqual(self.compensation.intervention, self.kom_old.intervention)
self.eiv = self.compensation.intervention
self.oek_old = self.create_dummy_eco_account()
self.eiv_old.responsible.conservation_office = self.conservation_office
self.eiv_old.legal.binding_date = old_date
self.eiv_old.legal.registration_date = old_date
self.eiv.responsible.conservation_office = self.conservation_office
self.eiv.legal.binding_date = today
self.eiv.legal.registration_date = today
self.eco_account.responsible.conservation_office = self.conservation_office
self.eco_account.legal.registration_date = today
self.eco_account.legal.binding_date = today
self.oek_old.responsible.conservation_office = self.conservation_office
self.oek_old.legal.registration_date = old_date
self.oek_old.legal.binding_date = old_date
self.eiv.legal.save()
self.eiv.responsible.save()
self.eiv_old.legal.save()
self.eiv_old.responsible.save()
self.eco_account.legal.save()
self.eco_account.responsible.save()
self.oek_old.legal.save()
self.oek_old.responsible.save()
self.deduction.account = self.eco_account
self.deduction.intervention = self.eiv
self.deduction.save()
def test_init(self):
date_from = now().date() - timedelta(days=365)
date_to = now().date()
report = TimespanReport(self.conservation_office.id, date_from, date_to)
self.assertEqual(report.office_id, self.conservation_office.id)
self.assertEqual(report.date_from, date_from)
self.assertEqual(report.date_to, date_to)
self.assertIsNotNone(report.intervention_report)
self.assertIsNotNone(report.compensation_report)
self.assertIsNotNone(report.eco_account_report)
self.assertIsNotNone(report.old_data_report)
self.assertEqual(report.excel_map["date_from"], date_from.strftime(DEFAULT_DATE_FORMAT))
self.assertEqual(report.excel_map["date_to"], date_to.strftime(DEFAULT_DATE_FORMAT))
self.assertEqual(report.old_data_report.queryset_intervention_count, 1)
self.assertEqual(report.old_data_report.queryset_intervention_recorded_count, 0)
self.assertEqual(report.old_data_report.queryset_comps_count, 1)
self.assertEqual(report.old_data_report.queryset_acc_count, 1)
self.assertEqual(report.old_data_report.queryset_acc_recorded_count, 0)
self.assertEqual(report.intervention_report.queryset_count, 1)
self.assertEqual(report.intervention_report.queryset_checked_count, 0)
self.assertEqual(report.intervention_report.queryset_recorded_count, 0)
self.assertEqual(report.compensation_report.queryset_count, 1)
self.assertEqual(report.compensation_report.queryset_checked_count, 0)
self.assertEqual(report.compensation_report.queryset_recorded_count, 0)
self.assertEqual(report.eco_account_report.queryset_count, 1)
self.assertEqual(report.eco_account_report.queryset_recorded_count, 0)
self.assertEqual(report.eco_account_report.queryset_deductions_count, 1)
self.assertEqual(report.eco_account_report.queryset_deductions_recorded_count, 0)

@ -413,6 +413,7 @@ class TimespanReport:
def __init__(self, id: str, date_from: str, date_to: str): def __init__(self, id: str, date_from: str, date_to: str):
# First fetch all eco account for this office # First fetch all eco account for this office
self.queryset = EcoAccount.objects.filter( self.queryset = EcoAccount.objects.filter(
legal__registration_date__gt=LKOMPVZVO_PUBLISH_DATE,
responsible__conservation_office__id=id, responsible__conservation_office__id=id,
deleted=None, deleted=None,
created__timestamp__date__gte=date_from, created__timestamp__date__gte=date_from,
@ -516,8 +517,8 @@ class TimespanReport:
legal__registration_date__lte=LKOMPVZVO_PUBLISH_DATE, legal__registration_date__lte=LKOMPVZVO_PUBLISH_DATE,
responsible__conservation_office__id=id, responsible__conservation_office__id=id,
deleted=None, deleted=None,
created__timestamp__gte=date_from, created__timestamp__date__gte=date_from,
created__timestamp__lte=date_to, created__timestamp__date__lte=date_to,
) )
self.queryset_acc_recorded = self.queryset_acc.filter( self.queryset_acc_recorded = self.queryset_acc.filter(
recorded__isnull=False, recorded__isnull=False,

@ -14,7 +14,7 @@ class APIUserToken(models.Model):
valid_until = models.DateField( valid_until = models.DateField(
blank=True, blank=True,
null=True, null=True,
help_text="Token is only valid until this date", help_text="Token is only valid until this date. Forever if null/blank.",
) )
is_active = models.BooleanField( is_active = models.BooleanField(
default=False, default=False,
@ -25,12 +25,11 @@ class APIUserToken(models.Model):
return self.token return self.token
@staticmethod @staticmethod
def get_user_from_token(token: str, username: str): def get_user_from_token(token: str):
""" Getter for the related user object """ Getter for the related user object
Args: Args:
token (str): The used token token (str): The used token
username (str): The username
Returns: Returns:
user (User): Otherwise None user (User): Otherwise None
@ -39,7 +38,6 @@ class APIUserToken(models.Model):
try: try:
token_obj = APIUserToken.objects.get( token_obj = APIUserToken.objects.get(
token=token, token=token,
user__username=username
) )
if not token_obj.is_active: if not token_obj.is_active:
raise PermissionError("Token unverified") raise PermissionError("Token unverified")

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 17.08.23
"""

@ -0,0 +1,71 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 17.08.23
"""
from datetime import timedelta
from django.utils.timezone import now
from api.models import APIUserToken
from konova.tests.test_views import BaseTestCase
class APIUserTokenTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.token = APIUserToken.objects.create()
self.superuser.api_token = self.token
self.superuser.save()
def test_str(self):
self.assertEqual(str(self.token), self.token.token)
def test_get_user_from_token(self):
a_day = timedelta(days=1)
today = now().date()
self.assertFalse(self.token.is_active)
self.assertIsNone(self.token.valid_until)
try:
#Token not existing --> fail
token_user = APIUserToken.get_user_from_token(self.token.token[::-1])
self.fail("There should not have been any token")
except PermissionError:
pass
try:
# Token not active --> fail
token_user = APIUserToken.get_user_from_token(self.token.token)
self.fail("Token is unverified but token user has been fetchable.")
except PermissionError:
pass
self.token.is_active = True
self.token.valid_until = today - a_day
self.token.save()
try:
# Token valid until yesterday --> fail
token_user = APIUserToken.get_user_from_token(self.token.token)
self.fail("Token reached end of lifetime but token user has been fetchable.")
except PermissionError:
pass
# Token valid until tomorrow --> success
self.token.valid_until = today + a_day
self.token.save()
token_user = APIUserToken.get_user_from_token(self.token.token)
self.assertEqual(token_user, self.superuser)
del token_user
# Token valid forever --> success
self.token.valid_until = None
self.token.save()
token_user = APIUserToken.get_user_from_token(self.token.token)
self.assertEqual(token_user, self.superuser)

@ -4,7 +4,6 @@ from django.urls import reverse
from konova.settings import DEFAULT_GROUP from konova.settings import DEFAULT_GROUP
from konova.tests.test_views import BaseTestCase from konova.tests.test_views import BaseTestCase
from konova.utils.user_checks import is_default_group_only
class BaseAPIV1TestCase(BaseTestCase): class BaseAPIV1TestCase(BaseTestCase):
@ -138,7 +137,7 @@ class APIV1SharingTestCase(BaseAPIV1TestCase):
# Give the user only default group rights # Give the user only default group rights
default_group = self.groups.get(name=DEFAULT_GROUP) default_group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([default_group]) self.superuser.groups.set([default_group])
self.assertTrue(is_default_group_only(self.superuser)) self.assertTrue(self.superuser.is_default_group_only())
# Add only him as shared_users an object # Add only him as shared_users an object
self.intervention.users.set([self.superuser]) self.intervention.users.set([self.superuser])

@ -18,7 +18,6 @@ from compensation.models import EcoAccount
from ema.models import Ema from ema.models import Ema
from intervention.models import Intervention from intervention.models import Intervention
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
from konova.utils.user_checks import is_default_group_only
from user.models import User, Team from user.models import User, Team
@ -53,7 +52,13 @@ class AbstractAPIView(View):
# Fetch the proper user from the given request header token # Fetch the proper user from the given request header token
ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None) ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None) ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
self.user = APIUserToken.get_user_from_token(ksp_token, ksp_user) token_user = APIUserToken.get_user_from_token(ksp_token)
if ksp_user != token_user.username:
raise PermissionError(f"Invalid token for {ksp_user}")
else:
self.user = token_user
request.user = self.user request.user = self.user
if not self.user.is_default_user(): if not self.user.is_default_user():
raise PermissionError("Default permissions required") raise PermissionError("Default permissions required")
@ -315,7 +320,7 @@ class AbstractModelShareAPIView(AbstractAPIView):
for team_name in new_teams: for team_name in new_teams:
new_teams_objs.append(Team.objects.get(name=team_name)) new_teams_objs.append(Team.objects.get(name=team_name))
if is_default_group_only(self.user): if self.user.is_default_group_only():
# Default only users are not allowed to remove other users from having access. They can only add new ones! # Default only users are not allowed to remove other users from having access. They can only add new ones!
new_users_to_be_added = User.objects.filter( new_users_to_be_added = User.objects.filter(
username__in=new_users username__in=new_users

@ -172,6 +172,23 @@ class EditEcoAccountForm(NewEcoAccountForm):
disabled_fields disabled_fields
) )
def is_valid(self):
valid = super().is_valid()
deductable_surface = self.cleaned_data.get("surface")
deduction_surface_sum = self.instance.get_deductions_surface()
if deductable_surface < deduction_surface_sum:
self.add_error(
"surface",
_("{}m² have been deducted from this eco account so far. The given value of {} would be too low.").format(
deduction_surface_sum,
deductable_surface
)
)
valid &= False
return valid
def save(self, user: User, geom_form: SimpleGeomForm): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
# Fetch data from cleaned POST values # Fetch data from cleaned POST values

@ -93,7 +93,7 @@ class NewCompensationActionModalForm(BaseModalForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("New action") self.form_title = _("New action")
self.form_caption = _("Insert data for the new action") self.form_caption = _("Insert data for the new action")
choices =KonovaCode.objects.filter( choices = KonovaCode.objects.filter(
code_lists__in=[CODELIST_COMPENSATION_ACTION_ID], code_lists__in=[CODELIST_COMPENSATION_ACTION_ID],
is_archived=False, is_archived=False,
is_leaf=True, is_leaf=True,

@ -77,8 +77,11 @@ class NewPaymentForm(BaseModalForm):
is_valid (bool): True if valid, False otherwise is_valid (bool): True if valid, False otherwise
""" """
super_valid = super().is_valid() super_valid = super().is_valid()
date = self.cleaned_data["due"] if not super_valid:
comment = self.cleaned_data["comment"] or None return super_valid
date = self.cleaned_data.get("due", None)
comment = self.cleaned_data.get("comment", None)
if not date and not comment: if not date and not comment:
# At least one needs to be set! # At least one needs to be set!
self.add_error( self.add_error(

@ -10,6 +10,7 @@ import shutil
from django.contrib import messages from django.contrib import messages
from codelist.models import KonovaCode from codelist.models import KonovaCode
from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION_IDENTIFIER_LENGTH
from user.models import User, Team from user.models import User, Team
from django.db import models, transaction from django.db import models, transaction
from django.db.models import QuerySet, Sum from django.db.models import QuerySet, Sum
@ -298,6 +299,9 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
objects = CompensationManager() objects = CompensationManager()
identifier_length = COMPENSATION_IDENTIFIER_LENGTH
identifier_template = COMPENSATION_IDENTIFIER_TEMPLATE
def __str__(self): def __str__(self):
return "{}".format(self.identifier) return "{}".format(self.identifier)
@ -395,7 +399,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
Returns: Returns:
users (QuerySet) users (QuerySet)
""" """
return self.intervention.users.all() return self.intervention.shared_users
@property @property
def shared_teams(self) -> QuerySet: def shared_teams(self) -> QuerySet:
@ -404,7 +408,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
Returns: Returns:
users (QuerySet) users (QuerySet)
""" """
return self.intervention.teams.all() return self.intervention.shared_teams
def get_documents(self) -> QuerySet: def get_documents(self) -> QuerySet:
""" Getter for all documents of a compensation """ Getter for all documents of a compensation
@ -417,19 +421,18 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
) )
return docs return docs
def mark_as_edited(self, user: User, request: HttpRequest = None, edit_comment: str = None, reset_recorded: bool = True): def mark_as_edited(self, user: User, request: HttpRequest = None, edit_comment: str = None):
""" Performs internal logic for setting the recordedd/checked state of the related intervention """ Performs internal logic for setting the checked state of the related intervention
Args: Args:
user (User): The performing user user (User): The performing user
request (HttpRequest): The performing request request (HttpRequest): The performing request
edit_comment (str): Additional comment for the log entry edit_comment (str): Additional comment for the log entry
reset_recorded (bool): Whether the record-state of the object should be reset
Returns: Returns:
""" """
self.intervention.unrecord(user, request) self.intervention.set_unchecked()
action = super().mark_as_edited(user, edit_comment=edit_comment) action = super().mark_as_edited(user, edit_comment=edit_comment)
return action return action
@ -509,8 +512,11 @@ class CompensationDocument(AbstractDocument):
# The only file left for this compensation is the one which is currently processed and will be deleted # The only file left for this compensation is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file # Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path # Therefore take the folder path from the file path
folder_path = self.file.path.split("/")[:-1] try:
folder_path = "/".join(folder_path) folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
except ValueError:
folder_path = None
if user: if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title)) self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))

@ -9,6 +9,7 @@ import shutil
from django.urls import reverse from django.urls import reverse
from compensation.settings import ECO_ACCOUNT_IDENTIFIER_TEMPLATE, ECO_ACCOUNT_IDENTIFIER_LENGTH
from konova.utils.message_templates import DEDUCTION_REMOVED, DOCUMENT_REMOVED_TEMPLATE from konova.utils.message_templates import DEDUCTION_REMOVED, DOCUMENT_REMOVED_TEMPLATE
from django.core.exceptions import ValidationError from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
@ -52,23 +53,12 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
objects = EcoAccountManager() objects = EcoAccountManager()
identifier_length = ECO_ACCOUNT_IDENTIFIER_LENGTH
identifier_template = ECO_ACCOUNT_IDENTIFIER_TEMPLATE
def __str__(self): def __str__(self):
return f"{self.identifier} ({self.title})" return f"{self.identifier} ({self.title})"
def clean(self):
# Deductable surface can not be larger than added states after surface
after_state_sum = self.get_surface_after_states()
if self.deductable_surface > after_state_sum:
raise ValidationError(_("Deductable surface can not be larger than existing surfaces in after states"))
# Deductable surface can not be lower than amount of already deducted surfaces
# User needs to contact deducting user in case of further problems
deducted_sum = self.get_deductions_surface()
if self.deductable_surface < deducted_sum:
raise ValidationError(
_("Deductable surface can not be smaller than the sum of already existing deductions. Please contact the responsible users for the deductions!")
)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
# Create new identifier if none was given # Create new identifier if none was given
@ -236,8 +226,11 @@ class EcoAccountDocument(AbstractDocument):
# The only file left for this eco account is the one which is currently processed and will be deleted # The only file left for this eco account is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file # Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path # Therefore take the folder path from the file path
folder_path = self.file.path.split("/")[:-1] try:
folder_path = "/".join(folder_path) folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
except ValueError:
folder_path = None
if user: if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title)) self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))

@ -10,8 +10,6 @@ from django.db import models
from intervention.models import Intervention from intervention.models import Intervention
from konova.models import BaseResource from konova.models import BaseResource
from konova.utils.message_templates import PAYMENT_REMOVED
from user.models import UserActionLogEntry
class Payment(BaseResource): class Payment(BaseResource):

@ -244,6 +244,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.client_user.post(record_url, post_data) self.client_user.post(record_url, post_data)
# Check that the intervention is still not recorded # Check that the intervention is still not recorded
self.intervention.refresh_from_db()
self.assertIsNone(self.intervention.recorded) self.assertIsNone(self.intervention.recorded)
# Now fill out the data for a compensation # Now fill out the data for a compensation

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 21.08.23
"""

@ -0,0 +1,318 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 21.08.23
"""
from django.core.exceptions import ObjectDoesNotExist
from django.test import RequestFactory
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCodeList
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID
from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \
EditCompensationActionModalForm, RemoveCompensationActionModalForm
from compensation.forms.modals.state import NewCompensationStateModalForm, EditCompensationStateModalForm, \
RemoveCompensationStateModalForm
from compensation.models import UnitChoices
from konova.tests.test_views import BaseTestCase
from konova.utils.generators import generate_random_string
from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, ADDED_COMPENSATION_ACTION, \
COMPENSATION_ACTION_REMOVED, ADDED_COMPENSATION_STATE, COMPENSATION_STATE_EDITED, \
COMPENSATION_STATE_REMOVED
from user.models import UserAction
class NewCompensationActionModalFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.superuser
self.action_dummy_code = self.create_dummy_codes().first()
action_list = KonovaCodeList.objects.get_or_create(
id=CODELIST_COMPENSATION_ACTION_ID,
)[0]
action_list.codes.add(self.action_dummy_code)
def test_init(self):
form = NewCompensationActionModalForm()
self.assertEqual(form.form_title, str(_("New action")))
self.assertEqual(form.form_caption, str(_("Insert data for the new action")))
self.assertTrue(len(form.fields["action_type"].choices) == 1)
def test_save(self):
comment = "TEST_comment"
unit = UnitChoices.km
amount = 2.5
data = {
"action_type": [self.action_dummy_code.id],
"action_type_details": [],
"unit": unit,
"amount": amount,
"comment": comment,
}
form = NewCompensationActionModalForm(data, request=self.request, instance=self.compensation)
self.assertTrue(form.is_valid(), msg=form.errors)
comp_action = form.save()
last_log = self.compensation.log.first()
self.assertIn(comp_action, self.compensation.actions.all())
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, ADDED_COMPENSATION_ACTION)
self.assertEqual(comp_action.amount, amount)
self.assertEqual(comp_action.unit, unit)
self.assertEqual(comp_action.comment, comment)
comp_action_types = comp_action.action_type.all()
self.assertEqual(comp_action_types.count(), 1)
self.assertEqual(comp_action_types.first(), self.action_dummy_code)
class EditCompensationActionModalFormTestCase(NewCompensationActionModalFormTestCase):
def setUp(self) -> None:
super().setUp()
self.comp_action = self.create_dummy_action()
self.compensation.actions.add(self.comp_action)
def test_init(self):
form = EditCompensationActionModalForm(request=self.request, instance=self.compensation, action=self.comp_action)
self.assertEqual(form.form_title, str(_("Edit action")))
self.assertEqual(len(form.fields["action_type"].initial), self.comp_action.action_type.count())
self.assertEqual(len(form.fields["action_type_details"].initial), self.comp_action.action_type_details.count())
self.assertEqual(form.fields["amount"].initial, self.comp_action.amount)
self.assertEqual(form.fields["unit"].initial, self.comp_action.unit)
self.assertEqual(form.fields["comment"].initial, self.comp_action.comment)
def test_save(self):
amount = 25.4
unit = UnitChoices.cm
comment = generate_random_string(length=20, use_numbers=True, use_letters_lc=True, use_letters_uc=True)
data = {
"action_type": [self.action_dummy_code.id],
"action_type_details": [],
"amount": amount,
"unit": unit,
"comment": comment,
}
form = EditCompensationActionModalForm(data, request=self.request, instance=self.compensation, action=self.comp_action)
self.assertTrue(form.is_valid())
action = form.save()
self.assertEqual(action.action_type.count(), len(data["action_type"]))
self.assertEqual(action.action_type_details.count(), 0)
self.assertEqual(float(action.amount), amount)
self.assertEqual(action.unit, unit)
self.assertEqual(action.comment, comment)
last_log = self.compensation.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, COMPENSATION_ACTION_EDITED)
self.assertIn(action, self.compensation.actions.all())
self.assertEqual(self.compensation.actions.count(), 1)
class RemoveCompensationActionModalFormTestCase(EditCompensationActionModalFormTestCase):
def setUp(self) -> None:
super().setUp()
def test_init(self):
self.assertIn(self.comp_action, self.compensation.actions.all())
form = RemoveCompensationActionModalForm(request=self.request, instance=self.compensation, action=self.comp_action)
self.assertEqual(form.action, self.comp_action)
def test_save(self):
data = {
"confirm": True,
}
form = RemoveCompensationActionModalForm(
data,
request=self.request,
instance=self.compensation,
action=self.comp_action
)
self.assertTrue(form.is_valid())
self.assertIn(self.comp_action, self.compensation.actions.all())
form.save()
last_log = self.compensation.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, COMPENSATION_ACTION_REMOVED)
self.assertNotIn(self.comp_action, self.compensation.actions.all())
try:
self.comp_action.refresh_from_db()
self.fail(msg="This action should not be fetchable anymore")
except ObjectDoesNotExist:
pass
class NewCompensationStateModalFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.superuser
self.comp_biotope_code = self.create_dummy_codes().first()
self.biotope_codelist = KonovaCodeList.objects.get_or_create(
id=CODELIST_BIOTOPES_ID
)[0]
self.biotope_codelist.codes.add(self.comp_biotope_code)
def test_init(self):
form = NewCompensationStateModalForm(request=self.request, instance=self.compensation)
self.assertEqual(form.form_title, str(_("New state")))
self.assertEqual(form.form_caption, str(_("Insert data for the new state")))
self.assertEqual(len(form.fields["biotope_type"].choices), 1)
def test_save(self):
test_surface = 123.45
data = {
"biotope_type": self.comp_biotope_code.id,
"biotope_extra": [],
"surface": test_surface,
}
self.assertEqual(self.compensation.before_states.count(), 0)
self.assertEqual(self.compensation.after_states.count(), 0)
form = NewCompensationStateModalForm(data, request=self.request, instance=self.compensation)
self.assertTrue(form.is_valid(), msg=form.errors)
is_before_state = True
state = form.save(is_before_state)
self.assertEqual(self.compensation.before_states.count(), 1)
self.assertEqual(self.compensation.after_states.count(), 0)
self.assertIn(state, self.compensation.before_states.all())
self.assertEqual(state.biotope_type, self.comp_biotope_code)
self.assertEqual(state.biotope_type_details.count(), 0)
self.assertEqual(float(state.surface), test_surface)
last_log = self.compensation.log.first()
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, ADDED_COMPENSATION_STATE)
is_before_state = False
state = form.save(is_before_state)
self.assertEqual(self.compensation.before_states.count(), 1)
self.assertEqual(self.compensation.after_states.count(), 1)
self.assertIn(state, self.compensation.after_states.all())
self.assertEqual(state.biotope_type, self.comp_biotope_code)
self.assertEqual(state.biotope_type_details.count(), 0)
self.assertEqual(float(state.surface), test_surface)
last_log = self.compensation.log.first()
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, ADDED_COMPENSATION_STATE)
class EditCompensationStateModalFormTestCase(NewCompensationStateModalFormTestCase):
def setUp(self) -> None:
super().setUp()
self.comp_state.biotope_type = self.comp_biotope_code
self.comp_state.save()
self.compensation.after_states.add(self.comp_state)
def test_init(self):
form = EditCompensationStateModalForm(request=self.request, instance=self.compensation, state=self.comp_state)
self.assertEqual(form.state, self.comp_state)
self.assertEqual(form.form_title, str(_("Edit state")))
self.assertEqual(form.fields["biotope_type"].initial, self.comp_state.biotope_type.id)
self.assertTrue(
form.fields["biotope_extra"].initial.difference(
self.comp_state.biotope_type_details.all()
).count() == 0
)
self.assertEqual(form.fields["surface"].initial, self.comp_state.surface)
def test_save(self):
test_surface = 987.65
test_code = self.create_dummy_codes().exclude(
id=self.comp_biotope_code.id
).first()
self.biotope_codelist.codes.add(test_code)
self.assertEqual(self.compensation.after_states.count(), 1)
self.assertEqual(self.compensation.before_states.count(), 0)
data = {
"biotope_type": test_code.id,
"biotope_extra": [],
"surface": test_surface,
}
form = EditCompensationStateModalForm(
data,
request=self.request,
instance=self.compensation,
state=self.comp_state
)
self.assertTrue(form.is_valid(), msg=form.errors)
is_before_state = False
state = form.save(is_before_state=is_before_state)
self.assertEqual(state.biotope_type, test_code)
self.assertEqual(state.biotope_type_details.count(), 0)
self.assertEqual(float(state.surface), test_surface)
last_log = self.compensation.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, COMPENSATION_STATE_EDITED)
class RemoveCompensationStateModalFormTestCase(EditCompensationStateModalFormTestCase):
def setUp(self) -> None:
super().setUp()
def test_init(self):
form = RemoveCompensationStateModalForm(request=self.request, instance=self.compensation, state=self.comp_state)
self.assertEqual(form.state, self.comp_state)
def test_save(self):
data = {
"confirm": True
}
form = RemoveCompensationStateModalForm(
data,
request=self.request,
instance=self.compensation,
state=self.comp_state
)
self.assertTrue(form.is_valid(), msg=form.errors)
self.assertIn(self.comp_state, self.compensation.after_states.all())
self.assertNotIn(self.comp_state, self.compensation.before_states.all())
form.save()
self.assertEqual(self.compensation.after_states.count(), 0)
self.assertEqual(self.compensation.before_states.count(), 0)
try:
self.comp_state.refresh_from_db()
self.fail("Entry should not existing anymore")
except ObjectDoesNotExist:
pass
last_log = self.compensation.log.first()
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, COMPENSATION_STATE_REMOVED)

@ -0,0 +1,201 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 30.08.23
"""
from django.core.exceptions import ObjectDoesNotExist
from django.test import RequestFactory
from django.utils.timezone import now
from compensation.forms.modals.deadline import NewDeadlineModalForm
from compensation.models import CompensationDocument
from konova.forms.modals import RemoveDeadlineModalForm
from konova.models import DeadlineType
from konova.tests.test_views import BaseTestCase
from konova.utils.message_templates import DEADLINE_REMOVED, DOCUMENT_REMOVED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, \
DEADLINE_ADDED
from user.models import UserAction, Team
class AbstractCompensationModelTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.superuser
def test_remove_deadline(self):
self.compensation.deadlines.add(self.finished_deadline)
data = {
"confirm": True
}
form = RemoveDeadlineModalForm(
data,
request=self.request,
instance=self.compensation,
deadline=self.finished_deadline,
)
self.assertTrue(form.is_valid(), msg=form.errors)
self.assertIn(self.finished_deadline, self.compensation.deadlines.all())
form.save()
last_log = self.compensation.log.first()
self.assertEqual(last_log.user, self.request.user)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, DEADLINE_REMOVED)
self.assertNotIn(self.finished_deadline, self.compensation.deadlines.all())
try:
self.finished_deadline.refresh_from_db()
self.fail("Deadline should not exist anymore after removing from abstract compensation")
except ObjectDoesNotExist:
pass
def test_add_deadline(self):
request = RequestFactory().request()
request.user = self.superuser
data = {
"type": DeadlineType.MAINTAIN,
"date": now().date(),
"comment": "TestDeadline"
}
form = NewDeadlineModalForm(
data,
request=self.request,
instance=self.compensation,
)
self.assertTrue(form.is_valid(), msg=form.errors)
deadline = self.compensation.add_deadline(form)
self.assertEqual(deadline.date, data["date"])
self.assertEqual(deadline.type, data["type"])
self.assertEqual(deadline.comment, data["comment"])
self.assertEqual(deadline.created.action, UserAction.CREATED)
self.assertEqual(deadline.created.user, self.superuser)
self.assertEqual(deadline.created.comment, None)
self.assertIn(deadline, self.compensation.deadlines.all())
last_log = self.compensation.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, DEADLINE_ADDED)
class CompensationTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_str(self):
self.assertEqual(str(self.compensation), self.compensation.identifier)
def test_save(self):
old_identifier = self.compensation.identifier
self.compensation.identifier = None
self.compensation.save()
self.assertIsNotNone(self.compensation.identifier)
self.assertNotEqual(old_identifier, self.compensation.identifier)
def test_share_with_user(self):
self.assertNotIn(self.user, self.compensation.shared_users)
self.compensation.share_with_user(self.user)
self.assertIn(self.user, self.compensation.shared_users)
def test_share_with_user_list(self):
user_list = [
self.user
]
self.assertNotIn(self.user, self.compensation.shared_users)
self.compensation.share_with_user_list(user_list)
self.assertIn(self.user, self.compensation.shared_users)
user_list = [
self.superuser
]
self.assertNotIn(self.superuser, self.compensation.shared_users)
self.compensation.share_with_user_list(user_list)
self.assertIn(self.superuser, self.compensation.shared_users)
self.assertNotIn(self.user, self.compensation.shared_users)
def test_share_with_team(self):
self.assertNotIn(self.team, self.compensation.shared_teams)
self.compensation.share_with_team(self.team)
self.assertIn(self.team, self.compensation.shared_teams)
def test_share_with_team_list(self):
self.compensation.share_with_team(self.team)
self.assertIn(self.team, self.compensation.shared_teams)
other_team = Team.objects.create(
name="NewTeam"
)
team_list = [
other_team
]
self.compensation.share_with_team_list(team_list)
self.assertIn(other_team, self.compensation.shared_teams)
self.assertNotIn(self.team, self.compensation.shared_teams)
def test_shared_users(self):
intervention = self.compensation.intervention
diff = self.compensation.shared_users.difference(intervention.shared_users)
self.assertEqual(diff.count(), 0)
self.compensation.share_with_user(self.superuser)
diff = self.compensation.shared_users.difference(intervention.shared_users)
self.assertEqual(diff.count(), 0)
def test_shared_teams(self):
intervention = self.compensation.intervention
diff = self.compensation.shared_users.difference(intervention.shared_users)
self.assertEqual(diff.count(), 0)
self.compensation.share_with_user(self.superuser)
diff = self.compensation.shared_users.difference(intervention.shared_users)
self.assertEqual(diff.count(), 0)
def test_get_documents(self):
doc = self.create_dummy_document(CompensationDocument, self.compensation)
docs = self.compensation.get_documents()
self.assertIn(doc, docs)
def test_mark_as_deleted(self):
self.assertIsNone(self.compensation.deleted)
self.compensation.mark_as_deleted(self.superuser, send_mail=False)
comp_deleted = self.compensation.deleted
self.assertIsNotNone(comp_deleted)
self.assertEqual(comp_deleted.action, UserAction.DELETED)
self.assertEqual(comp_deleted.user, self.superuser)
self.assertEqual(comp_deleted.comment, None)
intervention_last_log = self.compensation.intervention.log.first()
self.assertEqual(intervention_last_log.action, UserAction.EDITED)
self.assertEqual(intervention_last_log.user, self.superuser)
self.assertEqual(
intervention_last_log.comment,
COMPENSATION_REMOVED_TEMPLATE.format(
self.compensation.identifier
)
)
class CompensationDocumentTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.doc = self.create_dummy_document(CompensationDocument, self.compensation)
def test_delete(self):
doc_title = self.doc.title
self.assertIn(self.doc, self.compensation.get_documents())
self.doc.delete(self.superuser)
self.assertNotIn(self.doc, self.compensation.get_documents())
try:
self.doc.refresh_from_db()
self.fail("Document should not be fetchable anymore")
except ObjectDoesNotExist:
pass
last_log = self.compensation.log.first()
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, DOCUMENT_REMOVED_TEMPLATE.format(doc_title))

@ -106,14 +106,17 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
"surface": test_deductable_surface, "surface": test_deductable_surface,
"conservation_office": test_conservation_office.id "conservation_office": test_conservation_office.id
} }
self.client_user.post(url, post_data) response = self.client_user.post(url, post_data)
self.assertEqual(response.status_code, 302, msg=f"{response.content.decode('utf-8')}")
self.eco_account.refresh_from_db() self.eco_account.refresh_from_db()
deductions_surface = self.eco_account.get_deductions_surface()
check_on_elements = { check_on_elements = {
self.eco_account.title: new_title, self.eco_account.title: new_title,
self.eco_account.identifier: new_identifier, self.eco_account.identifier: new_identifier,
self.eco_account.deductable_surface: test_deductable_surface, self.eco_account.deductable_surface: test_deductable_surface,
self.eco_account.deductable_rest: test_deductable_surface, self.eco_account.deductable_rest: test_deductable_surface - deductions_surface,
self.eco_account.comment: new_comment, self.eco_account.comment: new_comment,
} }
@ -223,7 +226,9 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.eco_account.refresh_from_db() self.eco_account.refresh_from_db()
self.assertEqual(1, self.eco_account.deductions.count()) self.assertEqual(1, self.eco_account.deductions.count())
self.assertEqual(1, self.intervention.deductions.count()) self.assertEqual(1, self.intervention.deductions.count())
deduction = self.eco_account.deductions.first() deduction = self.eco_account.deductions.get(
surface=test_surface
)
self.assertEqual(deduction.surface, test_surface) self.assertEqual(deduction.surface, test_surface)
self.assertEqual(self.eco_account.deductable_rest, self.eco_account.deductable_surface - deduction.surface) self.assertEqual(self.eco_account.deductable_rest, self.eco_account.deductable_surface - deduction.surface)
self.assertEqual(deduction.account, self.eco_account) self.assertEqual(deduction.account, self.eco_account)

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 30.08.23
"""

@ -0,0 +1,128 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 30.08.23
"""
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from compensation.models import EcoAccountDocument
from konova.tests.test_views import BaseTestCase
from konova.utils.message_templates import DOCUMENT_REMOVED_TEMPLATE, DEDUCTION_REMOVED
from user.models import UserAction
class EcoAccountTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_str(self):
self.assertEqual(str(self.eco_account), f"{self.eco_account.identifier} ({self.eco_account.title})")
def test_save(self):
old_id = self.eco_account.identifier
self.assertIsNotNone(self.eco_account.identifier)
self.eco_account.identifier = None
self.eco_account.save()
self.assertIsNotNone(self.eco_account.identifier)
self.assertNotEqual(old_id, self.eco_account.identifier)
def test_property_deductions_surface_sum(self):
self.assertEqual(
self.eco_account.deductions_surface_sum,
self.eco_account.get_deductions_surface()
)
def test_get_documents(self):
docs = self.eco_account.get_documents()
self.assertEqual(docs.count(), 0)
doc = self.create_dummy_document(EcoAccountDocument, self.eco_account)
self.assertIn(doc, self.eco_account.get_documents())
def test_get_share_link(self):
self.assertEqual(
self.eco_account.get_share_link(),
reverse(
"compensation:acc:share-token",
args=(self.eco_account.id, self.eco_account.access_token)
)
)
def test_get_deductable_rest_relative(self):
self.assertEqual(self.eco_account.deductions.count(), 0)
self.eco_account.deductable_surface = 5.0
self.eco_account.save()
self.eco_account.update_deductable_rest()
self.assertEqual(self.eco_account.get_deductable_rest_relative(), 100)
self.eco_account.deductable_surface = None
self.eco_account.save()
self.assertEqual(self.eco_account.get_deductable_rest_relative(), 0)
class EcoAccountDocumentTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_delete(self):
doc = self.create_dummy_document(
EcoAccountDocument,
self.eco_account
)
doc_title = doc.title
docs = self.eco_account.get_documents()
self.assertIn(doc, docs)
doc.delete(user=self.superuser)
last_log = self.eco_account.log.first()
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, DOCUMENT_REMOVED_TEMPLATE.format(
doc_title
))
try:
doc.refresh_from_db()
self.fail("Document should not have been fetchable")
except ObjectDoesNotExist:
pass
class EcoAccountDeductionTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_str(self):
self.assertEqual(str(self.deduction), f"{self.deduction.surface} of {self.deduction.account}")
def test_delete(self):
self.deduction.account = self.eco_account
self.deduction.intervention = self.intervention
self.deduction.save()
self.eco_account.update_deductable_rest()
old_deductable_rest = self.eco_account.deductable_rest
deduction_surface = self.deduction.surface
self.deduction.delete(self.superuser)
last_log_intervention = self.intervention.log.first()
last_log_account = self.eco_account.log.first()
logs = [
last_log_intervention,
last_log_account,
]
for log in logs:
self.assertEqual(log.action, UserAction.EDITED)
self.assertEqual(log.user, self.superuser)
self.assertEqual(log.comment, DEDUCTION_REMOVED)
self.assertLess(old_deductable_rest, self.eco_account.deductable_rest)
self.assertEqual(old_deductable_rest + deduction_surface, self.eco_account.deductable_rest)
try:
self.deduction.refresh_from_db()
self.fail("Deduction still fetchable after deleting")
except ObjectDoesNotExist:
pass

@ -25,9 +25,8 @@ from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
RECORDED_BLOCKS_EDIT, CHECKED_RECORDED_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \ RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \
COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
from konova.utils.user_checks import in_group
@login_required @login_required
@ -170,15 +169,14 @@ def edit_view(request: HttpRequest, id: str):
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp) geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
if request.method == "POST": if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid(): if data_form.is_valid() and geom_form.is_valid():
# Preserve state of intervention recorded/checked to determine whether the user must be informed or not # Preserve state of intervention checked to determine whether the user must be informed or not
# about a change of the recorded/checked state # about a change of the check state
intervention_recorded = comp.intervention.recorded is not None intervention_is_checked = comp.intervention.checked is not None
intervention_checked = comp.intervention.checked is not None
# The data form takes the geom form for processing, as well as the performing user # The data form takes the geom form for processing, as well as the performing user
comp = data_form.save(request.user, geom_form) comp = data_form.save(request.user, geom_form)
if intervention_recorded or intervention_checked: if intervention_is_checked:
messages.info(request, CHECKED_RECORDED_RESET) messages.info(request, CHECK_STATE_RESET)
messages.success(request, _("Compensation {} edited").format(comp.identifier)) messages.success(request, _("Compensation {} edited").format(comp.identifier))
if geom_form.geometry_simplified: if geom_form.geometry_simplified:
messages.info( messages.info(
@ -266,9 +264,9 @@ def detail_view(request: HttpRequest, id: str):
"sum_before_states": sum_before_states, "sum_before_states": sum_before_states,
"sum_after_states": sum_after_states, "sum_after_states": sum_after_states,
"diff_states": diff_states, "diff_states": diff_states,
"is_default_member": in_group(_user, DEFAULT_GROUP), "is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP), "is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP), "is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": comp.get_LANIS_link(), "LANIS_LINK": comp.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}", TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}",
"has_finished_deadlines": comp.get_finished_deadlines().exists(), "has_finished_deadlines": comp.get_finished_deadlines().exists(),

@ -23,7 +23,6 @@ from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \ from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
from konova.utils.user_checks import in_group
@login_required @login_required
@ -150,7 +149,9 @@ def edit_view(request: HttpRequest, id: str):
data_form = EditEcoAccountForm(request.POST or None, instance=acc) data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc) geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
if request.method == "POST": if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid(): data_form_valid = data_form.is_valid()
geom_form_valid = geom_form.is_valid()
if data_form_valid and geom_form_valid:
# The data form takes the geom form for processing, as well as the performing user # The data form takes the geom form for processing, as well as the performing user
acc = data_form.save(request.user, geom_form) acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier)) messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
@ -242,9 +243,9 @@ def detail_view(request: HttpRequest, id: str):
"diff_states": diff_states, "diff_states": diff_states,
"available": available_relative, "available": available_relative,
"available_total": available_total, "available_total": available_total,
"is_default_member": in_group(_user, DEFAULT_GROUP), "is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP), "is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP), "is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": acc.get_LANIS_link(), "LANIS_LINK": acc.get_LANIS_link(),
"deductions": deductions, "deductions": deductions,
"actions": actions, "actions": actions,
@ -275,7 +276,7 @@ def remove_view(request: HttpRequest, id: str):
# default group user # default group user
if acc.recorded is not None or acc.deductions.exists(): if acc.recorded is not None or acc.deductions.exists():
user = request.user user = request.user
if not in_group(user, ETS_GROUP): if not user.in_group(ETS_GROUP):
messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED) messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED)
return redirect("compensation:acc:detail", id=id) return redirect("compensation:acc:detail", id=id)

@ -76,7 +76,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
) )
# Finally create main object # Finally create main object
acc = Ema.objects.create( ema = Ema.objects.create(
identifier=identifier, identifier=identifier,
title=title, title=title,
responsible=responsible, responsible=responsible,
@ -87,16 +87,16 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
) )
# Add the creating user to the list of shared users # Add the creating user to the list of shared users
acc.share_with_user(user) ema.share_with_user(user)
# Add the log entry to the main objects log list # Add the log entry to the main objects log list
acc.log.add(action) ema.log.add(action)
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!) # Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action) geometry = geom_form.save(action)
acc.geometry = geometry ema.geometry = geometry
acc.save() ema.save()
return acc return ema
class EditEmaForm(NewEmaForm): class EditEmaForm(NewEmaForm):

@ -15,6 +15,7 @@ from django.urls import reverse
from compensation.models import AbstractCompensation, PikMixin from compensation.models import AbstractCompensation, PikMixin
from ema.managers import EmaManager from ema.managers import EmaManager
from ema.settings import EMA_IDENTIFIER_LENGTH, EMA_IDENTIFIER_TEMPLATE
from ema.utils.quality import EmaQualityChecker from ema.utils.quality import EmaQualityChecker
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE
@ -38,6 +39,9 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, Pik
""" """
objects = EmaManager() objects = EmaManager()
identifier_length = EMA_IDENTIFIER_LENGTH
identifier_template = EMA_IDENTIFIER_TEMPLATE
def __str__(self): def __str__(self):
return "{}".format(self.identifier) return "{}".format(self.identifier)
@ -122,7 +126,7 @@ class EmaDocument(AbstractDocument):
def delete(self, user=None, *args, **kwargs): def delete(self, user=None, *args, **kwargs):
""" """
Custom delete functionality for EcoAccountDocuments. Custom delete functionality for EmaDocuments.
Removes the folder from the file system if there are no further documents for this entry. Removes the folder from the file system if there are no further documents for this entry.
Args: Args:
@ -139,8 +143,11 @@ class EmaDocument(AbstractDocument):
# The only file left for this EMA is the one which is currently processed and will be deleted # The only file left for this EMA is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file # Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path # Therefore take the folder path from the file path
folder_path = self.file.path.split("/")[:-1] try:
folder_path = "/".join(folder_path) folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
except ValueError:
folder_path = None
if user: if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title)) self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))

@ -6,5 +6,5 @@ Created on: 19.08.21
""" """
EMA_ACCOUNT_IDENTIFIER_LENGTH = 6 EMA_IDENTIFIER_LENGTH = 6
EMA_ACCOUNT_IDENTIFIER_TEMPLATE = "EMA-{}" EMA_IDENTIFIER_TEMPLATE = "EMA-{}"

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 24.08.23
"""

@ -0,0 +1,141 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 01.09.23
"""
import json
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from ema.forms import NewEmaForm, EditEmaForm
from konova.forms import SimpleGeomForm
from konova.tests.test_views import BaseTestCase
from konova.utils.generators import generate_random_string
from user.models import UserAction
class NewEmaFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_init(self):
form = NewEmaForm()
self.assertEqual(form.form_title, str(_("New EMA")))
self.assertEqual(form.action_url, reverse("ema:new"))
self.assertEqual(form.cancel_redirect, reverse("ema:index"))
self.assertIsNotNone(form.fields["identifier"].initial)
self.assertEqual(form.fields["title"].widget.attrs["placeholder"], str(_("Compensation XY; Location ABC")))
def test_save(self):
cons_office_code = self.get_conservation_office_code()
data = {
"identifier": generate_random_string(length=20, use_numbers=True),
"title": generate_random_string(length=20, use_letters_lc=True),
"conservation_office": cons_office_code,
"conservation_file_number": generate_random_string(length=10, use_numbers=True),
"is_pik": True,
"comment": generate_random_string(length=20, use_numbers=True, use_letters_uc=True),
}
form = NewEmaForm(data)
test_geom = self.create_dummy_geometry()
geom_form_data = self.create_geojson(
test_geom
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)
self.assertTrue(form.is_valid(), msg=form.errors)
self.assertTrue(geom_form.is_valid(), msg=form.errors)
obj = form.save(user=self.superuser, geom_form=geom_form)
self.assertEqual(obj.title, data["title"])
self.assertEqual(obj.is_pik, data["is_pik"])
self.assertIsNotNone(obj.responsible)
self.assertIsNotNone(obj.responsible.handler)
self.assertEqual(obj.responsible.conservation_office, data["conservation_office"])
self.assertEqual(obj.responsible.conservation_file_number, data["conservation_file_number"])
self.assertEqual(obj.identifier, data["identifier"])
self.assertEqual(obj.comment, data["comment"])
self.assertIn(self.superuser, obj.shared_users)
last_log = obj.log.first()
self.assertEqual(obj.created, obj.modified)
self.assertEqual(obj.created, last_log)
self.assertEqual(last_log.action, UserAction.CREATED)
self.assertEqual(last_log.user, self.superuser)
self.assertTrue(test_geom.equals_exact(obj.geometry.geom, 0.000001))
class EditEmaFormTestCase(BaseTestCase):
def test_init(self):
form = EditEmaForm(instance=self.ema)
self.assertEqual(form.form_title, str(_("Edit EMA")))
self.assertEqual(form.action_url, reverse("ema:edit", args=(self.ema.id,)))
self.assertEqual(form.cancel_redirect, reverse("ema:detail", args=(self.ema.id,)))
self.assertEqual(form.fields["identifier"].widget.attrs["url"], reverse("ema:new-id"))
self.assertEqual(form.fields["title"].widget.attrs["placeholder"], str(_("Compensation XY; Location ABC")))
values = {
"identifier": self.ema.identifier,
"title": self.ema.title,
"comment": self.ema.comment,
"conservation_office": self.ema.responsible.conservation_office,
"conservation_file_number": self.ema.responsible.conservation_file_number,
"is_pik": self.ema.is_pik,
"handler_type": self.ema.responsible.handler.type,
"handler_detail": self.ema.responsible.handler.detail,
}
for k, v in values.items():
self.assertEqual(form.fields[k].initial, v)
def test_save(self):
cons_office_code = self.get_conservation_office_code()
data = {
"identifier": generate_random_string(length=20, use_numbers=True),
"title": generate_random_string(length=20, use_letters_lc=True),
"conservation_office": cons_office_code,
"conservation_file_number": generate_random_string(length=10, use_numbers=True),
"is_pik": not self.ema.is_pik,
"comment": generate_random_string(length=20, use_numbers=True, use_letters_uc=True),
}
form = EditEmaForm(data, instance=self.ema)
self.assertTrue(form.is_valid(), msg=form.errors)
test_geom = self.create_dummy_geometry()
geom_form_data = self.create_geojson(
test_geom
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)
self.assertTrue(geom_form.is_valid())
obj = form.save(self.superuser, geom_form)
self.assertEqual(obj.id, self.ema.id)
self.assertEqual(obj.title, data["title"])
self.assertEqual(obj.is_pik, data["is_pik"])
self.assertIsNotNone(obj.responsible)
self.assertIsNotNone(obj.responsible.handler)
self.assertEqual(obj.responsible.conservation_office, data["conservation_office"])
self.assertEqual(obj.responsible.conservation_file_number, data["conservation_file_number"])
self.assertEqual(obj.identifier, data["identifier"])
self.assertEqual(obj.comment, data["comment"])
last_log = obj.log.first()
self.assertEqual(obj.modified, last_log)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertTrue(test_geom.equals_exact(obj.geometry.geom, 0.000001))

@ -0,0 +1,90 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 24.08.23
"""
from django.urls import reverse
from django.utils.timezone import now
from ema.models import Ema, EmaDocument
from ema.settings import EMA_IDENTIFIER_TEMPLATE
from konova.tests.test_views import BaseTestCase
from konova.utils.message_templates import DOCUMENT_REMOVED_TEMPLATE
from user.models import UserAction
class EmaModelTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_str(self):
self.assertEqual(str(self.ema), f"{self.ema.identifier}")
def test_save(self):
new_ema = Ema(
title="Test"
)
self.assertIsNone(new_ema.identifier)
new_ema.save()
new_ema.refresh_from_db()
self.assertIsNotNone(new_ema.identifier)
self.assertIn("EMA-", new_ema.identifier)
def test_is_ready_for_publish(self):
self.assertIsNone(self.ema.recorded)
self.assertFalse(self.ema.is_ready_for_publish())
self.ema.set_recorded(self.superuser)
self.ema.refresh_from_db()
self.assertIsNotNone(self.ema.recorded)
self.assertTrue(self.ema.is_ready_for_publish())
def test_get_share_link(self):
self.assertEqual(
self.ema.get_share_link(),
reverse("ema:share-token", args=(self.ema.id, self.ema.access_token))
)
def test_get_documents(self):
self.assertEqual(self.ema.get_documents().count(), 0)
doc = EmaDocument(
instance=self.ema,
date_of_creation=now().date(),
comment="Test",
)
doc.save()
docs = self.ema.get_documents()
self.assertEqual(docs.count(), 1)
self.assertEqual(docs.first(), doc)
class EmaDocumentModelTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_delete(self):
doc = EmaDocument.objects.create(
date_of_creation=now().date(),
instance=self.ema,
comment="TEST"
)
self.ema.refresh_from_db()
docs = self.ema.get_documents()
self.assertEqual(docs.count(), 1)
self.assertEqual(docs.first(), doc)
doc_title = doc.title
doc.delete(user=self.superuser)
last_log = self.ema.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, DOCUMENT_REMOVED_TEMPLATE.format(doc_title))
docs = self.ema.get_documents()
self.assertEqual(docs.count(), 0)

@ -24,7 +24,6 @@ from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \ from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
from konova.utils.user_checks import in_group
@login_required @login_required
@ -172,9 +171,9 @@ def detail_view(request: HttpRequest, id: str):
"sum_before_states": sum_before_states, "sum_before_states": sum_before_states,
"sum_after_states": sum_after_states, "sum_after_states": sum_after_states,
"diff_states": diff_states, "diff_states": diff_states,
"is_default_member": in_group(_user, DEFAULT_GROUP), "is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP), "is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP), "is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": ema.get_LANIS_link(), "LANIS_LINK": ema.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}", TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}",
"has_finished_deadlines": ema.get_finished_deadlines().exists(), "has_finished_deadlines": ema.get_finished_deadlines().exists(),

@ -43,6 +43,7 @@ class CheckModalForm(BaseModalForm):
""" """
deductions = self.instance.deductions.all() deductions = self.instance.deductions.all()
valid = True
for deduction in deductions: for deduction in deductions:
checker = deduction.account.quality_check() checker = deduction.account.quality_check()
for msg in checker.messages: for msg in checker.messages:
@ -50,8 +51,8 @@ class CheckModalForm(BaseModalForm):
"checked_comps", "checked_comps",
f"{deduction.account.identifier}: {msg}" f"{deduction.account.identifier}: {msg}"
) )
return checker.valid valid &= checker.valid
return True return valid
def _are_comps_valid(self): def _are_comps_valid(self):
""" Performs validity checks on all types of compensations """ Performs validity checks on all types of compensations

@ -12,7 +12,6 @@ from django.utils.translation import gettext_lazy as _
from intervention.inputs import TextToClipboardInput from intervention.inputs import TextToClipboardInput
from konova.forms.modals import BaseModalForm from konova.forms.modals import BaseModalForm
from konova.utils.message_templates import ENTRY_REMOVE_MISSING_PERMISSION from konova.utils.message_templates import ENTRY_REMOVE_MISSING_PERMISSION
from konova.utils.user_checks import is_default_group_only
from user.models import Team, User from user.models import Team, User
@ -80,7 +79,7 @@ class ShareModalForm(BaseModalForm):
teams = self.cleaned_data.get("teams", Team.objects.none()) teams = self.cleaned_data.get("teams", Team.objects.none())
_is_valid = True _is_valid = True
if is_default_group_only(self.user): if self.user.is_default_group_only():
shared_users = self.instance.shared_users shared_users = self.instance.shared_users
shared_teams = self.instance.shared_teams shared_teams = self.instance.shared_teams

@ -14,7 +14,7 @@ from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from analysis.settings import LKOMPVZVO_PUBLISH_DATE from analysis.settings import LKOMPVZVO_PUBLISH_DATE
from compensation.models import EcoAccountDeduction from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE
from intervention.tasks import celery_export_to_egon from intervention.tasks import celery_export_to_egon
from user.models import User from user.models import User
from django.db import models, transaction from django.db import models, transaction
@ -61,6 +61,9 @@ class Intervention(BaseObject,
objects = InterventionManager() objects = InterventionManager()
identifier_length = INTERVENTION_IDENTIFIER_LENGTH
identifier_template = INTERVENTION_IDENTIFIER_TEMPLATE
def __str__(self): def __str__(self):
return f"{self.identifier} ({self.title})" return f"{self.identifier} ({self.title})"
@ -276,22 +279,20 @@ class Intervention(BaseObject,
revocation.delete() revocation.delete()
self.mark_as_edited(user, request=form.request, edit_comment=REVOCATION_REMOVED) self.mark_as_edited(user, request=form.request, edit_comment=REVOCATION_REMOVED)
def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None, reset_recorded: bool = True): def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None):
""" In case the object or a related object changed, internal processes need to be started, such as """ Log the edit action
unrecord and uncheck
If the object is checked, set it to unchecked due to the editing. Another check is needed then.
Args: Args:
performing_user (User): The user which performed the editing action performing_user (User): The user which performed the editing action
request (HttpRequest): The used request for this action request (HttpRequest): The used request for this action
edit_comment (str): Additional comment for the log entry edit_comment (str): Additional comment for the log entry
reset_recorded (bool): Whether the record-state of the object should be reset
Returns: Returns:
""" """
action = super().mark_as_edited(performing_user, edit_comment=edit_comment) action = super().mark_as_edited(performing_user, edit_comment=edit_comment)
if reset_recorded:
self.unrecord(performing_user, request)
if self.checked: if self.checked:
self.set_unchecked() self.set_unchecked()
return action return action
@ -416,8 +417,11 @@ class InterventionDocument(AbstractDocument):
# The only file left for this intervention is the one which is currently processed and will be deleted # The only file left for this intervention is the one which is currently processed and will be deleted
# Make sure that the intervention folder itself is deleted as well, not only the file # Make sure that the intervention folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path # Therefore take the folder path from the file path
folder_path = self.file.path.split("/")[:-1] try:
folder_path = "/".join(folder_path) folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
except ValueError:
folder_path = None
if user: if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title)) self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))

@ -400,12 +400,13 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
self.eco_account.share_with_user_list([self.superuser]) self.eco_account.share_with_user_list([self.superuser])
self.eco_account.save() self.eco_account.save()
num_all_deducs = EcoAccountDeduction.objects.count() num_all_deducs = EcoAccountDeduction.objects.count()
num_acc_deducs = self.eco_account.deductions.count()
# Run the request # Run the request
self.client_user.post(new_url, post_data) self.client_user.post(new_url, post_data)
# Expect the deduction to be created, since all constraints are fulfilled # Expect the deduction to be created, since all constraints are fulfilled
self.assertEqual(1, self.eco_account.deductions.count()) self.assertEqual(num_acc_deducs + 1, self.eco_account.deductions.count())
self.assertEqual(num_all_deducs + 1, EcoAccountDeduction.objects.count()) self.assertEqual(num_all_deducs + 1, EcoAccountDeduction.objects.count())
# Make sure the deduction contains the expected data # Make sure the deduction contains the expected data

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 24.08.23
"""

@ -0,0 +1,333 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 24.08.23
"""
import json
from datetime import timedelta
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from django.test import RequestFactory
from intervention.forms.intervention import NewInterventionForm, EditInterventionForm
from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \
RemoveRevocationModalForm
from intervention.forms.modals.share import ShareModalForm
from intervention.models import Revocation
from konova.forms import SimpleGeomForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP
from konova.tests.test_views import BaseTestCase
from konova.utils.generators import generate_random_string
from konova.utils.message_templates import REVOCATION_ADDED, REVOCATION_EDITED, REVOCATION_REMOVED
from user.models import UserAction
class NewInterventionFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_init(self):
form = NewInterventionForm()
self.assertEqual(form.form_title, str(_("New intervention")))
self.assertEqual(form.action_url, reverse("intervention:new"))
self.assertEqual(form.cancel_redirect, reverse("intervention:index"))
initial_identifier = form.fields["identifier"].initial
self.assertIsNotNone(initial_identifier)
self.assertIn("EIV-", initial_identifier)
def test_is_valid(self):
data = {
"identifier": generate_random_string(length=15, use_letters_uc=True),
"title": generate_random_string(length=15, use_letters_uc=True),
}
form = NewInterventionForm({})
self.assertFalse(form.is_valid())
form = NewInterventionForm(data)
self.assertTrue(form.is_valid(), msg=form.errors)
def test_save(self):
data = {
"identifier": generate_random_string(length=15, use_letters_uc=True),
"title": generate_random_string(length=15, use_letters_uc=True),
}
test_geom = self.create_dummy_geometry()
geom_form_data = self.create_geojson(
test_geom
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)
form = NewInterventionForm(data)
self.assertTrue(form.is_valid())
self.assertTrue(geom_form.is_valid())
obj = form.save(self.superuser, geom_form)
self.assertEqual(obj.identifier, data["identifier"])
self.assertEqual(obj.title, data["title"])
self.assertIsNotNone(obj.legal)
self.assertIsNotNone(obj.responsible)
self.assertIsNotNone(obj.responsible.handler)
self.assertEqual(obj.created.action, UserAction.CREATED)
self.assertEqual(obj.created.user, self.superuser)
self.assertEqual(obj.created, obj.log.first())
self.assertEqual(obj.created, obj.modified)
self.assertIn(self.superuser, obj.shared_users)
self.assertTrue(test_geom.equals_exact(obj.geometry.geom, 0.000001))
class EditInterventionFormTestCase(NewInterventionFormTestCase):
def test_init(self):
today = now().date()
data = {
"identifier": self.intervention.identifier,
"title": generate_random_string(length=5, use_letters_lc=True),
"comment": generate_random_string(length=5, use_letters_lc=True),
"registration_date": today,
"binding_date": today,
"registration_file_number": generate_random_string(length=5, use_numbers=True),
"conservation_file_number": generate_random_string(length=5, use_numbers=True),
}
test_geom = self.create_dummy_geometry()
geom_form_data = self.create_geojson(
test_geom
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)
form = EditInterventionForm(data, instance=self.intervention)
self.assertTrue(geom_form.is_valid())
self.assertTrue(form.is_valid())
obj = form.save(self.superuser, geom_form)
last_log = obj.log.first()
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log, obj.modified)
self.assertEqual(obj.identifier, self.intervention.identifier)
self.assertIsNotNone(obj.legal)
self.assertIsNotNone(obj.responsible)
self.assertIsNotNone(obj.responsible.handler)
self.assertEqual(obj.title, data["title"])
self.assertEqual(obj.comment, data["comment"])
self.assertTrue(test_geom.equals_exact(obj.geometry.geom, 0.000001))
self.assertEqual(obj.legal.binding_date, today)
self.assertEqual(obj.legal.registration_date, today)
self.assertEqual(obj.responsible.registration_file_number, data["registration_file_number"])
self.assertEqual(obj.responsible.conservation_file_number, data["conservation_file_number"])
class ShareModalFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.superuser
def test_init(self):
self.intervention.access_token = None
self.intervention.save()
form = ShareModalForm(
request=self.request,
instance=self.intervention
)
self.assertIsNotNone(self.intervention.access_token)
self.assertEqual(form.form_title, str(_("Share")))
self.assertEqual(form.form_caption, str(_("Share settings for {}").format(
self.intervention.identifier
)))
self.assertEqual(form.template, "modal/modal_form.html")
self.assertEqual(form.instance, self.intervention)
self.assertEqual(form.user, self.superuser)
def test_is_valid_and_save(self):
# make user default-group-only (special treatment)
self.superuser.groups.set(
self.groups.filter(
name=DEFAULT_GROUP
)
)
self.assertNotIn(self.superuser, self.intervention.shared_users)
self.assertNotIn(self.team, self.intervention.shared_teams)
# Add new sharing data
## Default-only is able to add new sharing but can not remove existing ones
data = {
"users": [self.superuser.id,],
"teams": [self.team.id,],
}
form = ShareModalForm(
data,
request=self.request,
instance=self.intervention,
)
self.assertTrue(form.is_valid(), msg=form.errors)
form.save()
self.assertIn(self.superuser, self.intervention.shared_users)
self.assertIn(self.team, self.intervention.shared_teams)
# Try to remove sharing data das default-only user
data = {
"users": [],
"teams": [],
}
form = ShareModalForm(
data,
request=self.request,
instance=self.intervention,
)
self.assertFalse(form.is_valid(), msg=form.errors)
self.assertTrue(form.has_error("teams"))
self.assertTrue(form.has_error("users"))
# Add another permission group for user
self.superuser.groups.add(
self.groups.get(
name=ZB_GROUP
)
)
form = ShareModalForm(
data,
request=self.request,
instance=self.intervention,
)
self.assertTrue(form.is_valid(), msg=form.errors)
self.assertIn(self.superuser, self.intervention.shared_users)
self.assertIn(self.team, self.intervention.shared_teams)
form.save()
self.assertNotIn(self.superuser, self.intervention.shared_users)
self.assertNotIn(self.team, self.intervention.shared_teams)
class NewRevocationModalFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.superuser
def test_init(self):
form = NewRevocationModalForm(
request=self.request,
instance=self.intervention
)
self.assertEqual(form.instance, self.intervention)
self.assertEqual(form.user, self.request.user)
self.assertEqual(form.request, self.request)
self.assertEqual(form.form_title, str(_("Add revocation")))
self.assertEqual(form.form_caption, "")
self.assertEqual(form.form_attrs, {
"enctype": "multipart/form-data"
})
def test_save(self):
data = {
"date": now().date(),
"file": None,
"comment": generate_random_string(20, use_letters_uc=True)
}
form = NewRevocationModalForm(
data,
request=self.request,
instance=self.intervention
)
self.assertTrue(form.is_valid(), msg=form.errors)
obj = form.save()
self.assertEqual(obj.intervention, self.intervention)
self.assertEqual(obj.date, data["date"])
self.assertEqual(obj.legal, self.intervention.legal)
self.assertEqual(obj.comment, data["comment"])
last_log = self.intervention.log.first()
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, REVOCATION_ADDED)
class EditRevocationModalFormTestCase(NewRevocationModalFormTestCase):
def setUp(self) -> None:
super().setUp()
self.revoc = Revocation.objects.get_or_create(
date=now().date(),
comment="TEST",
legal=self.intervention.legal,
)[0]
def test_init(self):
new_date = now().date() - timedelta(days=10)
data = {
"date": new_date,
"comment": generate_random_string(20, use_letters_lc=True)
}
form = EditRevocationModalForm(
data,
request=self.request,
instance=self.intervention,
revocation=self.revoc
)
self.assertTrue(form.is_valid(), msg=form.errors)
obj = form.save()
self.assertEqual(obj.date, new_date)
self.assertEqual(obj.comment, data["comment"])
self.assertEqual(obj.legal, self.intervention.legal)
self.assertEqual(obj.intervention, self.intervention)
last_log = self.intervention.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, REVOCATION_EDITED)
class RemoveRevocationModalFormTestCase(EditRevocationModalFormTestCase):
def setUp(self) -> None:
super().setUp()
def test_init(self):
form = RemoveRevocationModalForm(
request=self.request,
instance=self.intervention,
revocation=self.revoc,
)
self.assertEqual(form.instance, self.intervention)
self.assertEqual(form.revocation, self.revoc)
self.assertEqual(form.request, self.request)
self.assertEqual(form.user, self.request.user)
def test_save(self):
data = {
"confirm": True,
}
form = RemoveRevocationModalForm(
data,
request=self.request,
instance=self.intervention,
revocation=self.revoc
)
self.assertTrue(form.is_valid(), msg=form.errors)
form.save()
try:
self.revoc.refresh_from_db()
self.fail("Revocation should not exist anymore")
except ObjectDoesNotExist:
pass
last_log = self.intervention.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, REVOCATION_REMOVED)

@ -0,0 +1,50 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 07.09.23
"""
from django.core.exceptions import ObjectDoesNotExist
from django.utils.timezone import now
from intervention.models import RevocationDocument, Revocation
from konova.tests.test_views import BaseTestCase
class RevocationDocumentTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.revocation = Revocation.objects.get_or_create(
date=now().date(),
comment="Test",
legal=self.intervention.legal
)[0]
self.doc = self.create_dummy_document(
RevocationDocument,
instance=self.revocation
)
def test_intervention_property(self):
self.assertEqual(
self.doc.intervention,
self.doc.instance.legal.intervention
)
self.assertEqual(
self.doc.intervention,
self.intervention
)
def test_delete(self):
revoc_docs, other_intervention_docs = self.intervention.get_documents()
self.assertIn(self.doc, revoc_docs)
try:
self.doc.delete()
self.doc.refresh_from_db()
self.fail("Should not be fetchable anymore!")
except ObjectDoesNotExist:
pass
revoc_docs, other_intervention_docs = self.intervention.get_documents()
self.assertEqual(revoc_docs.count(), 0)

@ -22,8 +22,7 @@ from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \
CHECKED_RECORDED_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
from konova.utils.user_checks import in_group
@login_required @login_required
@ -186,9 +185,9 @@ def detail_view(request: HttpRequest, id: str):
"compensations": compensations, "compensations": compensations,
"has_access": is_data_shared, "has_access": is_data_shared,
"geom_form": geom_form, "geom_form": geom_form,
"is_default_member": in_group(_user, DEFAULT_GROUP), "is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP), "is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP), "is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": intervention.get_LANIS_link(), "LANIS_LINK": intervention.get_LANIS_link(),
"has_payment_without_document": has_payment_without_document, "has_payment_without_document": has_payment_without_document,
TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}", TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}",
@ -230,12 +229,11 @@ def edit_view(request: HttpRequest, id: str):
if data_form.is_valid() and geom_form.is_valid(): if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user # The data form takes the geom form for processing, as well as the performing user
# Save the current state of recorded|checked to inform the user in case of a status reset due to editing # Save the current state of recorded|checked to inform the user in case of a status reset due to editing
i_rec = intervention.recorded is not None intervention_is_checked = intervention.checked is not None
i_check = intervention.checked is not None
intervention = data_form.save(request.user, geom_form) intervention = data_form.save(request.user, geom_form)
messages.success(request, _("Intervention {} edited").format(intervention.identifier)) messages.success(request, _("Intervention {} edited").format(intervention.identifier))
if i_check or i_rec: if intervention_is_checked:
messages.info(request, CHECKED_RECORDED_RESET) messages.info(request, CHECK_STATE_RESET)
if geom_form.geometry_simplified: if geom_form.geometry_simplified:
messages.info( messages.info(
request, request,

@ -8,10 +8,10 @@ Created on: 15.08.22
import json import json
from django.contrib.gis import gdal from django.contrib.gis import gdal
from django.contrib.gis.forms import MultiPolygonField
from django.contrib.gis.geos import MultiPolygon, Polygon from django.contrib.gis.geos import MultiPolygon, Polygon
from django.contrib.gis.geos.prototypes.io import WKTWriter from django.contrib.gis.geos.prototypes.io import WKTWriter
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.forms import JSONField
from konova.forms.base_form import BaseForm from konova.forms.base_form import BaseForm
from konova.models import Geometry from konova.models import Geometry
@ -27,8 +27,7 @@ class SimpleGeomForm(BaseForm):
""" """
read_only = True read_only = True
geometry_simplified = False geometry_simplified = False
geom = MultiPolygonField( geom = JSONField(
srid=DEFAULT_SRID_RLP,
label=_("Geometry"), label=_("Geometry"),
help_text=_(""), help_text=_(""),
label_suffix="", label_suffix="",

@ -14,6 +14,12 @@ from user.models import UserActionLogEntry, User
class RemoveForm(BaseForm): class RemoveForm(BaseForm):
""" DEPRECATED
NOT USED IN ANY PLACE.
CAN BE DELETED AT SOME POINT.
"""
check = forms.BooleanField( check = forms.BooleanField(
label=_("Confirm"), label=_("Confirm"),
label_suffix=_(""), label_suffix=_(""),

@ -317,7 +317,7 @@ class Geometry(BaseResource):
""" """
geom = self.geom geom = self.geom
if geom.srid != srid: if geom.srid != srid:
geom.transform(ct=srid) geom.transform(srid)
polygons = [p for p in geom] polygons = [p for p in geom]
geojson = { geojson = {
"type": "FeatureCollection", "type": "FeatureCollection",

@ -23,13 +23,9 @@ from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.timezone import now from django.utils.timezone import now
from django.db import models, transaction from django.db import models, transaction
from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION_IDENTIFIER_LENGTH, \
ECO_ACCOUNT_IDENTIFIER_TEMPLATE, ECO_ACCOUNT_IDENTIFIER_LENGTH
from ema.settings import EMA_ACCOUNT_IDENTIFIER_LENGTH, EMA_ACCOUNT_IDENTIFIER_TEMPLATE
from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE
from konova.utils import generators from konova.utils import generators
from konova.utils.generators import generate_random_string from konova.utils.generators import generate_random_string
from konova.utils.message_templates import CHECKED_RECORDED_RESET, GEOMETRY_CONFLICT_WITH_TEMPLATE from konova.utils.message_templates import GEOMETRY_CONFLICT_WITH_TEMPLATE
class UuidModel(models.Model): class UuidModel(models.Model):
@ -143,6 +139,9 @@ class BaseObject(BaseResource, DeletableObjectMixin):
comment = models.TextField(null=True, blank=True) comment = models.TextField(null=True, blank=True)
log = models.ManyToManyField("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False) log = models.ManyToManyField("user.UserActionLogEntry", blank=True, help_text="Keeps all user actions of an object", editable=False)
identifier_length = 6 # Fallback - specified in inheriting classes
identifier_template = "UNBEKANNT-{}" # Fallback - specified in inheriting classes
class Meta: class Meta:
abstract = True abstract = True
@ -193,32 +192,8 @@ class BaseObject(BaseResource, DeletableObjectMixin):
Returns: Returns:
str str
""" """
from compensation.models import Compensation, EcoAccount id_len = self.identifier_length
from intervention.models import Intervention id_template = self.identifier_template
from ema.models import Ema
definitions = {
Intervention: {
"length": INTERVENTION_IDENTIFIER_LENGTH,
"template": INTERVENTION_IDENTIFIER_TEMPLATE,
},
Compensation: {
"length": COMPENSATION_IDENTIFIER_LENGTH,
"template": COMPENSATION_IDENTIFIER_TEMPLATE,
},
EcoAccount: {
"length": ECO_ACCOUNT_IDENTIFIER_LENGTH,
"template": ECO_ACCOUNT_IDENTIFIER_TEMPLATE,
},
Ema: {
"length": EMA_ACCOUNT_IDENTIFIER_LENGTH,
"template": EMA_ACCOUNT_IDENTIFIER_TEMPLATE,
},
}
if self.__class__ not in definitions:
# Not defined, yet. Create fallback identifier for this case
return generate_random_string(10)
_now = now() _now = now()
curr_month = _now.month curr_month = _now.month
@ -229,13 +204,13 @@ class BaseObject(BaseResource, DeletableObjectMixin):
curr_month = str(curr_month) curr_month = str(curr_month)
curr_year = str(_now.year) curr_year = str(_now.year)
rand_str = generate_random_string( rand_str = generate_random_string(
length=definitions[self.__class__]["length"], length=id_len,
use_numbers=True, use_numbers=True,
use_letters_lc=False, use_letters_lc=False,
use_letters_uc=True, use_letters_uc=True,
) )
_str = "{}{}-{}".format(curr_month, curr_year, rand_str) _str = "{}{}-{}".format(curr_month, curr_year, rand_str)
return definitions[self.__class__]["template"].format(_str) return id_template.format(_str)
@abstractmethod @abstractmethod
def get_detail_url(self): def get_detail_url(self):
@ -319,27 +294,6 @@ class RecordableObjectMixin(models.Model):
return action return action
def unrecord(self, performing_user, request: HttpRequest = None):
""" Unrecords a dataset
Args:
performing_user (User): The user which performed the editing action
request (HttpRequest): The used request for this action
Returns:
"""
action = None
if self.recorded:
action = self.set_unrecorded(performing_user)
self.log.add(action)
if request:
messages.info(
request,
CHECKED_RECORDED_RESET
)
return action
@abstractmethod @abstractmethod
def is_ready_for_publish(self) -> bool: def is_ready_for_publish(self) -> bool:
""" Check for all needed publishing-constraints on the data """ Check for all needed publishing-constraints on the data
@ -374,7 +328,7 @@ class CheckableObjectMixin(models.Model):
abstract = True abstract = True
def set_unchecked(self) -> None: def set_unchecked(self) -> None:
""" Perform unrecording """ Perform unchecking
Args: Args:
@ -384,7 +338,7 @@ class CheckableObjectMixin(models.Model):
if not self.checked: if not self.checked:
# Nothing to do # Nothing to do
return return
# Do not .delete() the checked attribute! Just set it to None, since a delete() would kill it out of the # Do not .delete() the checked attribute! Just set it to None, since a delete() would remove it from the
# log history, which is not what we want! # log history, which is not what we want!
self.checked = None self.checked = None
self.save() self.save()
@ -686,12 +640,11 @@ class ShareableObjectMixin(models.Model):
Returns: Returns:
""" """
from konova.utils.user_checks import is_default_group_only
users = self.shared_users users = self.shared_users
cleaned_users = [] cleaned_users = []
default_users = [] default_users = []
for user in users: for user in users:
if not is_default_group_only(user): if not user.is_default_group_only():
cleaned_users.append(user) cleaned_users.append(user)
else: else:
default_users.append(user) default_users.append(user)

@ -15,7 +15,7 @@ DEFAULT_SRID_RLP = 25832
# Needed to redirect to LANIS # Needed to redirect to LANIS
## Values to be inserted are [zoom_level, x_coord, y_coord] ## Values to be inserted are [zoom_level, x_coord, y_coord]
LANIS_LINK_TEMPLATE = "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/index.php?lang=de&zl={}&x={}&y={}&bl=tk_rlp_tms_grau&bo=1&lo=0.8,0.8,0.8,0.6,0.8,0.8,0.8,0.8,0.8&layers=eiv_recorded,eiv_unrecorded,kom_recorded,kom_unrecorded,oek_recorded,oek_unrecorded,ema_recorded,ema_unrecorded,mae&service=kartendienste_naturschutz" LANIS_LINK_TEMPLATE = "https://geodaten.naturschutz.rlp.de/kartendienste_naturschutz/index.php?lang=de&zl={}&x={}&y={}&bl=tk_rlp_tms_grau&bo=1&lo=0.8,0.8,0.8,0.6,0.8,0.8,0.8,0.8,0.8&layers=eiv_recorded,eiv_unrecorded,eiv_unrecorded_old_entries,kom_recorded,kom_unrecorded,kom_unrecorded_old_entries,oek_recorded,oek_unrecorded,ema_recorded,ema_unrecorded,mae&service=kartendienste_naturschutz"
## This look up table (LUT) defines different zoom levels on the size of the calculate area of a geometry. ## This look up table (LUT) defines different zoom levels on the size of the calculate area of a geometry.
LANIS_ZOOM_LUT = { LANIS_ZOOM_LUT = {
1000000000: 6, 1000000000: 6,

@ -5,9 +5,12 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.12.21 Created on: 15.12.21
""" """
import json
from django.contrib.gis.db.models.functions import Translate from django.contrib.gis.db.models.functions import Translate
from konova.models import Geometry, GeometryConflict from konova.models import Geometry, GeometryConflict
from konova.sub_settings.lanis_settings import DEFAULT_SRID
from konova.tests.test_views import BaseTestCase from konova.tests.test_views import BaseTestCase
from konova.utils.schneider.fetcher import ParcelFetcher from konova.utils.schneider.fetcher import ParcelFetcher
@ -74,3 +77,66 @@ class GeometryTestCase(BaseTestCase):
fetcher = ParcelFetcher(geometry=self.geom_1) fetcher = ParcelFetcher(geometry=self.geom_1)
features = fetcher.get_parcels() features = fetcher.get_parcels()
self.assertNotEqual(0, len(features), msg="Spatial fetcher get feature did not work!") self.assertNotEqual(0, len(features), msg="Spatial fetcher get feature did not work!")
def test_str(self):
self.assertEqual(
str(self.geom_1),
str(self.geom_1.id)
)
def test_get_data_objects(self):
num_objs_with_geom = 0
self.assertEqual(
len(self.geom_1.get_data_objects()),
num_objs_with_geom
)
objs = [
self.intervention,
self.compensation,
self.eco_account,
self.ema,
]
for obj in objs:
obj.geometry = self.geom_1
obj.save()
num_objs_with_geom += 1
geom_objs = self.geom_1.get_data_objects()
self.assertEqual(
len(geom_objs),
num_objs_with_geom
)
self.assertIn(obj, geom_objs)
def test_as_feature_collection(self):
geometry = self.geom_1.geom
polygons = [p for p in geometry]
expected_result = {
"type": "FeatureCollection",
"crs": {
"type": "name",
"properties": {
"name": f"urn:ogc:def:crs:EPSG::{geometry.srid}"
}
},
"features": [
{
"type": "Feature",
"geometry": json.loads(p.json),
}
for p in polygons
]
}
result = self.geom_1.as_feature_collection()
result = json.dumps(result)
expected_result = json.dumps(expected_result)
self.assertEqual(expected_result, result)
# Transform geometry into non-default-rlp srid to trigger retransforming in later steps
geometry.transform(DEFAULT_SRID)
different_result = self.geom_1.as_feature_collection()
different_result = json.dumps(result)
self.assertNotEqual(different_result, result)

@ -8,7 +8,7 @@ Created on: 26.10.21
import datetime import datetime
import json import json
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID
from ema.models import Ema from ema.models import Ema
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from user.models import User, Team from user.models import User, Team
@ -157,14 +157,17 @@ class BaseTestCase(TestCase):
intervention.generate_access_token(make_unique=True) intervention.generate_access_token(make_unique=True)
return intervention return intervention
def create_dummy_compensation(self): def create_dummy_compensation(self, interv: Intervention=None):
""" Creates a compensation which can be used for tests """ Creates a compensation which can be used for tests
Returns: Returns:
""" """
if self.intervention is None: if not interv:
self.intervention = self.create_dummy_intervention() if self.intervention is None:
interv = self.create_dummy_intervention()
else:
interv = self.intervention
# Create dummy data # Create dummy data
# Create log entry # Create log entry
action = UserActionLogEntry.get_created_action(self.superuser) action = UserActionLogEntry.get_created_action(self.superuser)
@ -173,7 +176,7 @@ class BaseTestCase(TestCase):
compensation = Compensation.objects.create( compensation = Compensation.objects.create(
identifier="TEST", identifier="TEST",
title="Test_title", title="Test_title",
intervention=self.intervention, intervention=interv,
created=action, created=action,
geometry=geometry, geometry=geometry,
comment="Test", comment="Test",
@ -196,9 +199,11 @@ class BaseTestCase(TestCase):
handler = self.handler handler = self.handler
responsible_data.handler = handler responsible_data.handler = handler
responsible_data.save() responsible_data.save()
identifier = EcoAccount().generate_new_identifier()
# Finally create main object, holding the other objects # Finally create main object, holding the other objects
eco_account = EcoAccount.objects.create( eco_account = EcoAccount.objects.create(
identifier="TEST", identifier=identifier,
title="Test_title", title="Test_title",
deductable_surface=500, deductable_surface=500,
legal=lega_data, legal=lega_data,
@ -234,10 +239,15 @@ class BaseTestCase(TestCase):
) )
return ema return ema
def create_dummy_deduction(self): def create_dummy_deduction(self, acc: EcoAccount = None, interv: Intervention = None):
if not acc:
acc = self.create_dummy_eco_account()
if not interv:
interv = self.create_dummy_intervention()
return EcoAccountDeduction.objects.create( return EcoAccountDeduction.objects.create(
account=self.create_dummy_eco_account(), account=acc,
intervention=self.create_dummy_intervention(), intervention=interv,
surface=100, surface=100,
) )
@ -270,15 +280,17 @@ class BaseTestCase(TestCase):
Returns: Returns:
""" """
codes = KonovaCode.objects.bulk_create([ codes = KonovaCode.objects.all()
KonovaCode(id=1, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test1"), if codes.count() == 0:
KonovaCode(id=2, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test2"), codes = KonovaCode.objects.bulk_create([
KonovaCode(id=3, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test3"), KonovaCode(id=1, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test1"),
KonovaCode(id=4, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test4"), KonovaCode(id=2, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test2"),
]) KonovaCode(id=3, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test3"),
KonovaCode(id=4, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test4"),
])
return codes return codes
def create_dummy_team(self): def create_dummy_team(self, name: str = None):
""" Creates a dummy team """ Creates a dummy team
Returns: Returns:
@ -287,8 +299,11 @@ class BaseTestCase(TestCase):
if self.superuser is None: if self.superuser is None:
self.create_users() self.create_users()
if not name:
name = "Testteam"
team = Team.objects.get_or_create( team = Team.objects.get_or_create(
name="Testteam", name=name,
description="Testdescription", description="Testdescription",
)[0] )[0]
team.users.add(self.superuser) team.users.add(self.superuser)
@ -318,7 +333,7 @@ class BaseTestCase(TestCase):
""" """
polygon = Polygon.from_bbox((7.592449, 50.359385, 7.593382, 50.359874)) polygon = Polygon.from_bbox((7.592449, 50.359385, 7.593382, 50.359874))
polygon.srid = 4326 polygon.srid = 4326
polygon = polygon.transform(DEFAULT_SRID_RLP, clone=True) polygon.transform(DEFAULT_SRID_RLP)
return MultiPolygon(polygon, srid=DEFAULT_SRID_RLP) return MultiPolygon(polygon, srid=DEFAULT_SRID_RLP)
def create_geojson(self, geometry): def create_geojson(self, geometry):
@ -408,6 +423,19 @@ class BaseTestCase(TestCase):
codelist.codes.add(code) codelist.codes.add(code)
return code return code
def get_registration_office_code(self):
""" Returns a dummy KonovaCode as conservation office code
Returns:
"""
codelist = KonovaCodeList.objects.get_or_create(
id=CODELIST_REGISTRATION_OFFICE_ID
)[0]
code = KonovaCode.objects.get(id=3)
codelist.codes.add(code)
return code
def fill_out_ema(self, ema): def fill_out_ema(self, ema):
""" Adds all required (dummy) data to an Ema """ Adds all required (dummy) data to an Ema

@ -0,0 +1,365 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 07.09.23
"""
import mimetypes
from datetime import timedelta
from django.core.exceptions import ObjectDoesNotExist
from django.test import RequestFactory
from django.utils.translation import gettext_lazy as _
from django.core.files.uploadedfile import SimpleUploadedFile
from django.utils.timezone import now
from compensation.forms.modals.document import NewEcoAccountDocumentModalForm, NewCompensationDocumentModalForm
from compensation.models import Payment
from ema.forms import NewEmaDocumentModalForm
from intervention.forms.modals.document import NewInterventionDocumentModalForm
from intervention.models import InterventionDocument
from konova.forms.modals import EditDocumentModalForm, NewDocumentModalForm, RecordModalForm, RemoveModalForm, \
RemoveDeadlineModalForm, ResubmissionModalForm
from konova.models import Resubmission
from konova.tests.test_views import BaseTestCase
from konova.utils.generators import generate_random_string
from konova.utils.message_templates import DOCUMENT_EDITED, DEADLINE_REMOVED
from user.models import UserAction
class NewDocumentModalFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.superuser
self.dummy_file = SimpleUploadedFile(
"some_file.pdf",
b"Some conent in this file",
mimetypes.types_map[".pdf"]
)
dummy_file_dict = {
"file": self.dummy_file
}
self.data = {
"title": generate_random_string(length=5, use_letters_lc=True),
"creation_date": now().date(),
"comment": generate_random_string(length=50, use_letters_uc=True),
}
self.forms = [
NewInterventionDocumentModalForm(self.data, dummy_file_dict, request=self.request, instance=self.intervention),
NewCompensationDocumentModalForm(self.data, dummy_file_dict, request=self.request, instance=self.compensation),
NewEmaDocumentModalForm(self.data, dummy_file_dict, request=self.request, instance=self.ema),
NewEcoAccountDocumentModalForm(self.data, dummy_file_dict, request=self.request, instance=self.eco_account),
]
def test_init(self):
for form in self.forms:
self.assertEqual(form.form_title, str(_("Add new document")))
self.assertEqual(form.form_caption, str(_("")))
self.assertEqual(
form.form_attrs,
{
"enctype": "multipart/form-data"
}
)
self.assertEqual(form.request, self.request)
self.assertEqual(form.user, self.superuser)
try:
NewDocumentModalForm(request=self.request, instance=self.intervention)
self.fail("Base form NewDocumentModalForm should not be creatable")
except NotImplementedError:
pass
def test_is_valid(self):
for form in self.forms:
self.assertTrue(
form.is_valid(), msg=form.errors
)
def test_save(self):
for form in self.forms:
form.is_valid()
obj = form.save()
self.assertEqual(obj.created.action, UserAction.CREATED)
self.assertEqual(obj.created.user, self.superuser)
self.assertEqual(obj.title, self.data["title"])
self.assertEqual(obj.date_of_creation, self.data["creation_date"])
self.assertEqual(obj.comment, self.data["comment"])
self.assertIsNotNone(obj.file)
last_log = obj.instance.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.request.user)
self.assertEqual(last_log.comment, str(_("Added document")))
self.assertEqual(obj.instance.modified, last_log)
class EditDocumentModalFormTestCase(NewDocumentModalFormTestCase):
def setUp(self) -> None:
super().setUp()
dummy_file_dict = {
"file": self.dummy_file
}
self.doc = self.create_dummy_document(
InterventionDocument,
instance=self.intervention
)
self.form = EditDocumentModalForm(
self.data,
dummy_file_dict,
request=self.request,
instance=self.intervention,
document=self.doc
)
def test_init(self):
self.assertEqual(self.form.form_title, str(_("Edit document")))
self.assertEqual(self.form.document, self.doc)
self.assertEqual(self.form.request, self.request)
self.assertEqual(self.form.user, self.request.user)
self.assertEqual(self.form.fields["title"].initial, self.doc.title)
self.assertEqual(self.form.fields["comment"].initial, self.doc.comment)
self.assertEqual(self.form.fields["creation_date"].initial, self.doc.date_of_creation)
self.assertEqual(self.form.fields["file"].initial, self.doc.file)
def test_save(self):
self.assertTrue(self.form.is_valid(), msg=self.form.errors)
obj = self.form.save()
self.assertEqual(obj.title, self.data["title"])
self.assertEqual(obj.comment, self.data["comment"])
self.assertEqual(obj.date_of_creation, self.data["creation_date"])
last_log = obj.instance.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.request.user)
self.assertEqual(last_log.comment, DOCUMENT_EDITED)
class RecordModalFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.user
self.fill_out_compensation(self.compensation)
def test_init(self):
form = RecordModalForm(
request=self.request,
instance=self.intervention
)
self.assertEqual(form.form_title, str(_("Record data")))
self.assertEqual(form.form_caption, str(
_("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(
self.user.first_name,
self.user.last_name
)
))
self.intervention.set_recorded(self.user)
self.intervention.refresh_from_db()
form = RecordModalForm(
request=self.request,
instance=self.intervention
)
self.assertEqual(form.form_title, str(_("Unrecord data")))
self.assertEqual(form.fields["confirm"].label, str(_("Confirm unrecord")))
self.assertEqual(form.form_caption, str(
_("I, {} {}, confirm that this data must be unrecorded.").format(
self.user.first_name,
self.user.last_name
)))
def test_is_valid(self):
data = {
"confirm": True
}
form = RecordModalForm(data, request=self.request, instance=self.intervention)
self.assertFalse(self.intervention.is_recorded)
self.assertFalse(form.is_valid(), msg=form.errors) # intervention not complete
self.intervention = self.fill_out_intervention(self.intervention)
form = RecordModalForm(data, request=self.request, instance=self.intervention)
self.assertTrue(form.is_valid(), msg=form.errors)
def test_save(self):
data = {
"confirm": True
}
self.intervention = self.fill_out_intervention(self.intervention)
form = RecordModalForm(data, request=self.request, instance=self.intervention)
self.assertTrue(form.is_valid(), msg=form.errors)
form.save()
self.assertEqual(self.intervention.recorded.action, UserAction.RECORDED)
self.assertEqual(self.intervention.recorded.user, self.request.user)
self.assertEqual(self.intervention.recorded, self.intervention.log.first())
class RemoveModalFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.user
def test_init(self):
form = RemoveModalForm(request=self.request, instance=self.intervention)
self.assertEqual(form.form_title, str(_("Remove")))
self.assertEqual(form.form_caption, str(_("Are you sure?")))
self.assertEqual(form.template, "modal/modal_form.html")
self.assertEqual(form.request, self.request)
self.assertEqual(form.user, self.request.user)
def test_save(self):
data = {
"confirm": True,
}
form = RemoveModalForm(
data,
request=self.request,
instance=self.intervention
)
self.assertTrue(form.is_valid(), msg=form.errors)
form.save()
self.assertEqual(self.intervention.deleted.action, UserAction.DELETED)
self.assertEqual(self.intervention.deleted.user, self.request.user)
payment = Payment.objects.create(
amount=1.0,
intervention=self.intervention
)
form = RemoveModalForm(
data,
request=self.request,
instance=payment
)
self.assertTrue(form.is_valid(), msg=form.errors)
form.save()
try:
payment.refresh_from_db()
self.fail("Payment still exists")
except ObjectDoesNotExist:
pass
class RemoveDeadlineTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
request = RequestFactory().request()
request.user = self.user
self.request = request
def test_init(self):
form = RemoveDeadlineModalForm(
request=self.request,
instance=self.compensation,
deadline=self.finished_deadline
)
self.assertEqual(form.form_title, str(_("Remove")))
self.assertEqual(form.form_caption, str(_("Are you sure?")))
self.assertEqual(form.template, "modal/modal_form.html")
self.assertEqual(form.request, self.request)
self.assertEqual(form.user, self.request.user)
def test_save(self):
self.compensation.deadlines.add(self.finished_deadline)
data = {
"confirm": True
}
form = RemoveDeadlineModalForm(
data,
request=self.request,
instance=self.compensation,
deadline=self.finished_deadline
)
self.assertTrue(form.is_valid(), msg=form.errors)
form.save()
try:
self.finished_deadline.refresh_from_db()
self.fail("Deadline still exists")
except ObjectDoesNotExist:
pass
last_log = self.compensation.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.request.user)
self.assertEqual(last_log.comment, DEADLINE_REMOVED)
class ResubmissionModalFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.user
def test_init(self):
# Resubmission nonexistent
form = ResubmissionModalForm(request=self.request, instance=self.intervention)
self.assertEqual(form.form_title, str(_("Resubmission")))
self.assertEqual(form.form_caption, str(_("Set your resubmission for this entry.")))
self.assertEqual(form.action_url, None)
self.assertIsNotNone(form.resubmission)
resubmission = Resubmission.objects.create(
user=self.request.user,
resubmit_on=now().date(),
comment=generate_random_string(length=10, use_letters_lc=True)
)
self.intervention.resubmissions.add(resubmission)
# Resubmission exists
form = ResubmissionModalForm(request=self.request, instance=self.intervention)
self.assertEqual(form.form_title, str(_("Resubmission")))
self.assertEqual(form.form_caption, str(_("Set your resubmission for this entry.")))
self.assertEqual(form.action_url, None)
self.assertEqual(form.fields["date"].initial, str(resubmission.resubmit_on))
self.assertEqual(form.fields["comment"].initial, resubmission.comment)
self.assertEqual(form.resubmission, resubmission)
def test_is_valid(self):
yesterday = now().date() - timedelta(days=1)
data = {
"date": yesterday,
"comment": "Edited comment"
}
form = ResubmissionModalForm(
data,
request=self.request,
instance=self.intervention
)
self.assertFalse(form.is_valid(), msg=form.errors)
self.assertTrue(form.has_error("date"))
tomorrow = yesterday + timedelta(days=2)
data = {
"date": tomorrow,
"comment": "Edited comment"
}
form = ResubmissionModalForm(
data,
request=self.request,
instance=self.intervention
)
self.assertTrue(form.is_valid(), msg=form.errors)
def test_save(self):
data = {
"date": now().date() + timedelta(days=1),
"comment": "New comment for new resubmission"
}
form = ResubmissionModalForm(
data,
request=self.request,
instance=self.intervention
)
self.assertTrue(form.is_valid(), msg=form.errors)
obj = form.save()
self.assertIn(obj, self.intervention.resubmissions.all())
self.assertEqual(obj.resubmit_on, data["date"])
self.assertEqual(obj.comment, data["comment"])
self.assertEqual(obj.resubmission_sent, False)
self.assertEqual(obj.user, self.request.user)

@ -0,0 +1,201 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 08.09.23
"""
from django.test import RequestFactory
from django.utils.timezone import now
from intervention.forms.modals.share import ShareModalForm
from konova.models import DeadlineType, Resubmission
from konova.settings import ZB_GROUP
from konova.tests.test_views import BaseTestCase
from user.models import UserAction
class DeadlineTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_str(self):
self.assertEqual(str(self.finished_deadline), self.finished_deadline.type)
def test_type_humanized_property(self):
self.assertEqual(self.finished_deadline.type_humanized, DeadlineType.FINISHED.label)
class BaseObjectTestCase(BaseTestCase):
def test_add_log_entry(self):
self.assertEqual(self.intervention.log.count(), 0)
self.intervention.add_log_entry(UserAction.EDITED, self.user, "TEST")
self.assertEqual(self.intervention.log.count(), 1)
last_log = self.intervention.log.first()
self.assertEqual(last_log.user, self.user)
self.assertEqual(last_log.comment, "TEST")
self.assertEqual(last_log.action, UserAction.EDITED)
def test_generate_new_identifier(self):
old_identifier = self.intervention.identifier
new_identifier = self.intervention.generate_new_identifier()
self.assertNotEqual(old_identifier, new_identifier)
class RecordableObjectMixinTestCase(BaseTestCase):
def test_set_recorded_and_set_unrecorded(self):
""" Tests set_unrecorded() as well
Returns:
"""
self.intervention.set_recorded(self.user)
self.assertIsNotNone(self.intervention.recorded)
self.assertEqual(self.intervention.recorded.user, self.user)
self.assertEqual(self.intervention.recorded.action, UserAction.RECORDED)
self.intervention.set_unrecorded(self.user)
self.assertIsNone(self.intervention.recorded)
last_log = self.intervention.log.first()
self.assertEqual(last_log.action, UserAction.UNRECORDED)
self.assertEqual(last_log.user, self.user)
class CheckableObjectMixinTestCase(BaseTestCase):
def test_set_unchecked_and_set_checked(self):
self.intervention.set_checked(self.user)
self.assertIsNotNone(self.intervention.checked)
self.assertEqual(self.intervention.checked.action, UserAction.CHECKED)
self.assertEqual(self.intervention.checked.user, self.user)
checked_action = self.intervention.checked
self.intervention.set_unchecked()
self.assertIsNone(self.intervention.checked)
# There is no explicit UNCHECKED UserAction since unchecking does never happen manually but only as an
# automatic consequence of editing an already checked entry. Therefore the last log entry in this case would
# be the checking of the entry
last_log = self.intervention.log.first()
self.assertEqual(last_log.action, UserAction.CHECKED)
self.assertEqual(last_log.user, self.user)
self.assertEqual(last_log, checked_action)
def test_get_last_checked_action(self):
self.intervention.set_checked(self.user)
action = self.intervention.checked
self.intervention.mark_as_edited(self.user)
last_log = self.intervention.log.first()
self.assertNotEqual(last_log, action)
last_check_action = self.intervention.get_last_checked_action()
self.assertEqual(action, last_check_action)
class ShareableObjectMixinTestCase(BaseTestCase):
def test_share_with_and_is_shared_with(self):
self.assertFalse(self.intervention.is_shared_with(self.user))
self.assertNotIn(self.user, self.intervention.shared_users)
self.intervention.share_with_user(self.user)
self.assertTrue(self.intervention.is_shared_with(self.user))
self.assertIn(self.user, self.intervention.shared_users)
self.assertTrue(self.intervention.is_only_shared_with(self.user))
self.assertFalse(self.intervention.is_only_shared_with(self.superuser))
self.assertNotIn(self.superuser, self.intervention.shared_users)
self.intervention.share_with_user(self.superuser)
self.assertFalse(self.intervention.is_only_shared_with(self.user))
self.assertIn(self.superuser, self.intervention.shared_users)
self.intervention.share_with_user_list([])
self.assertNotIn(self.superuser, self.intervention.shared_users)
self.assertNotIn(self.user, self.intervention.shared_users)
self.intervention.share_with_user_list([
self.superuser,
self.user
])
self.assertIn(self.superuser, self.intervention.shared_users)
self.assertIn(self.user, self.intervention.shared_users)
def test_share_with_team_and_team_list(self):
self.assertNotIn(self.team, self.intervention.shared_teams)
self.intervention.share_with_team(self.team)
self.assertIn(self.team, self.intervention.shared_teams)
another_team = self.create_dummy_team(name="Another team")
team_list = [
self.team,
another_team
]
self.assertNotIn(another_team, self.intervention.shared_teams)
self.intervention.share_with_team_list(team_list)
self.assertIn(another_team, self.intervention.shared_teams)
def test_update_shared_access(self):
another_team = self.create_dummy_team(name="Another team")
request = RequestFactory().request()
request.user = self.superuser
self.superuser.groups.add(
self.groups.get(name=ZB_GROUP)
)
self.intervention.share_with_team(another_team)
self.intervention.share_with_user(self.user)
self.assertTrue(self.intervention.is_shared_with(self.user))
self.assertIn(another_team, self.intervention.shared_teams)
data = {
"users": [
self.superuser.id,
],
"teams": [
self.team.id,
]
}
form = ShareModalForm(data, request=request, instance=self.intervention)
self.assertTrue(form.is_valid(), msg=form.errors)
form.save()
self.assertNotIn(self.user, self.intervention.shared_users)
self.assertNotIn(another_team, self.intervention.shared_teams)
self.assertIn(self.superuser, self.intervention.shared_users)
self.assertIn(self.team, self.intervention.shared_teams)
def test_unshare_with_default_users(self):
self.superuser.groups.add(
self.groups.get(
name=ZB_GROUP
)
)
self.intervention.share_with_user(self.user)
self.intervention.share_with_user(self.superuser)
self.assertTrue(self.user.is_default_group_only())
self.assertFalse(self.superuser.is_default_group_only())
self.assertTrue(self.intervention.is_shared_with(self.user))
self.assertTrue(self.intervention.is_shared_with(self.superuser))
self.intervention.unshare_with_default_users()
self.assertFalse(self.intervention.is_shared_with(self.user))
self.assertTrue(self.intervention.is_shared_with(self.superuser))
class ResubmissionTestCase(BaseTestCase):
def test_send_resubmission_mail(self):
resubmission = Resubmission.objects.create(
user=self.user,
resubmit_on=now().date(),
comment="Test",
)
self.intervention.resubmissions.add(resubmission)
self.assertFalse(resubmission.resubmission_sent)
resubmission.send_resubmission_mail(
self.intervention.identifier,
[
"Test_municipal_1"
],
)
self.assertTrue(resubmission.resubmission_sent)

@ -19,17 +19,17 @@ from django.urls import path, include
from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG from konova.settings import SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY, DEBUG
from konova.sso.sso import KonovaSSOClient from konova.sso.sso import KonovaSSOClient
from konova.views.logout import logout_view from konova.views.logout import LogoutView
from konova.views.geometry import get_geom_parcels, get_geom_parcels_content from konova.views.geometry import GeomParcelsView, GeomParcelsContentView
from konova.views.home import home_view from konova.views.home import HomeView
from konova.views.map_proxy import ClientProxyParcelSearch, ClientProxyParcelWFS from konova.views.map_proxy import ClientProxyParcelSearch, ClientProxyParcelWFS
sso_client = KonovaSSOClient(SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY) sso_client = KonovaSSOClient(SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY)
urlpatterns = [ urlpatterns = [
path('admin/', admin.site.urls), path('admin/', admin.site.urls),
path('login/', include(sso_client.get_urls())), path('login/', include(sso_client.get_urls())),
path('logout/', logout_view, name="logout"), path('logout/', LogoutView.as_view(), name="logout"),
path('', home_view, name="home"), path('', HomeView.as_view(), name="home"),
path('intervention/', include("intervention.urls")), path('intervention/', include("intervention.urls")),
path('compensation/', include("compensation.urls")), path('compensation/', include("compensation.urls")),
path('ema/', include("ema.urls")), path('ema/', include("ema.urls")),
@ -38,8 +38,8 @@ urlpatterns = [
path('cl/', include("codelist.urls")), path('cl/', include("codelist.urls")),
path('analysis/', include("analysis.urls")), path('analysis/', include("analysis.urls")),
path('api/', include("api.urls")), path('api/', include("api.urls")),
path('geom/<id>/parcels/', get_geom_parcels, name="geometry-parcels"), path('geom/<id>/parcels/', GeomParcelsView.as_view(), name="geometry-parcels"),
path('geom/<id>/parcels/<int:page>', get_geom_parcels_content, name="geometry-parcels-content"), path('geom/<id>/parcels/<int:page>', GeomParcelsContentView.as_view(), name="geometry-parcels-content"),
path('client/proxy', ClientProxyParcelSearch.as_view(), name="client-proxy-search"), path('client/proxy', ClientProxyParcelSearch.as_view(), name="client-proxy-search"),
path('client/proxy/wfs', ClientProxyParcelWFS.as_view(), name="client-proxy-wfs"), path('client/proxy/wfs', ClientProxyParcelWFS.as_view(), name="client-proxy-wfs"),
] ]
@ -50,4 +50,4 @@ if DEBUG:
] ]
handler404 = "konova.views.error.get_404_view" handler404 = "konova.views.error.get_404_view"
handler500 = "konova.views.error.get_500_view" handler500 = "konova.views.error.get_500_view"

@ -18,7 +18,7 @@ INTERVENTION_INVALID = _("There are errors in this intervention.")
IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since another entry has been added in the meanwhile, which uses this identifier") IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since another entry has been added in the meanwhile, which uses this identifier")
ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office users are allowed to remove entries.") ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office users are allowed to remove entries.")
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.") MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
CHECKED_RECORDED_RESET = _("Status of Checked and Recorded reseted") CHECK_STATE_RESET = _("Status of Checked reset")
RECORDED_BLOCKS_EDIT = _("Entry is recorded. To edit data, the entry first needs to be unrecorded.") RECORDED_BLOCKS_EDIT = _("Entry is recorded. To edit data, the entry first needs to be unrecorded.")
# SHARE # SHARE

@ -1,37 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 02.07.21
"""
from user.models import User
from konova.settings import ETS_GROUP, ZB_GROUP
def in_group(user: User, group: str) -> bool:
""" Checks if the user is part of a group
Args:
user (User): The user object
group (str): The group's name
Returns:
bool
"""
return user.groups.filter(
name=group
)
def is_default_group_only(user: User) -> bool:
""" Checks if the user is only part of the default group
Args:
user (User): The user object
Returns:
bool
"""
return not in_group(user, ZB_GROUP) and not in_group(user, ETS_GROUP)

@ -5,104 +5,110 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.gis.geos import MultiPolygon from django.contrib.gis.geos import MultiPolygon
from django.http import HttpResponse, HttpRequest from django.http import HttpResponse, HttpRequest
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.views import View
from konova.models import Geometry, Municipal from konova.models import Geometry
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
def get_geom_parcels(request: HttpRequest, id: str): class GeomParcelsView(LoginRequiredMixin, View):
""" Getter for HTMX
def get(self, request: HttpRequest, id: str):
Returns all parcels of the requested geometry rendered into a simple HTML table """ Getter for HTMX
Args: Returns all parcels of the requested geometry rendered into a simple HTML table
request (HttpRequest): The incoming request
id (str): The geometry's id Args:
request (HttpRequest): The incoming request
Returns: id (str): The geometry's id
A rendered piece of HTML
""" Returns:
# HTTP code 286 states that the HTMX should stop polling for updates A rendered piece of HTML
# https://htmx.org/docs/#polling """
status_code = 286 # HTTP code 286 states that the HTMX should stop polling for updates
template = "konova/includes/parcels/parcel_table_frame.html" # https://htmx.org/docs/#polling
geom = get_object_or_404(Geometry, id=id) status_code = 286
parcels = geom.get_underlying_parcels() template = "konova/includes/parcels/parcel_table_frame.html"
geos_geom = geom.geom or MultiPolygon(srid=DEFAULT_SRID_RLP) geom = get_object_or_404(Geometry, id=id)
parcels = geom.get_underlying_parcels()
geometry_exists = not geos_geom.empty geos_geom = geom.geom or MultiPolygon(srid=DEFAULT_SRID_RLP)
parcels_are_currently_calculated = geometry_exists and geos_geom.area > 0 and len(parcels) == 0
parcels_available = len(parcels) > 0 geometry_exists = not geos_geom.empty
parcels_are_currently_calculated = geometry_exists and geos_geom.area > 0 and len(parcels) == 0
if parcels_are_currently_calculated: parcels_available = len(parcels) > 0
# Parcels are being calculated right now. Change the status code, so polling stays active for fetching
# resutls after the calculation if parcels_are_currently_calculated:
status_code = 200 # Parcels are being calculated right now. Change the status code, so polling stays active for fetching
# resutls after the calculation
if parcels_available or not geometry_exists: status_code = 200
municipals = geom.get_underlying_municipals(parcels)
if parcels_available or not geometry_exists:
municipals = geom.get_underlying_municipals(parcels)
rpp = 100
num_all_parcels = parcels.count()
parcels = parcels[:rpp]
next_page = 1
if len(parcels) < rpp:
next_page = None
context = {
"num_parcels": num_all_parcels,
"parcels": parcels,
"municipals": municipals,
"geom_id": str(id),
"next_page": next_page,
}
html = render_to_string(template, context, request)
return HttpResponse(html, status=status_code)
else:
return HttpResponse(None, status=404)
class GeomParcelsContentView(LoginRequiredMixin, View):
def get(self, request: HttpRequest, id: str, page: int):
""" Getter for infinite scroll of HTMX
Returns parcels of a specific page/slice of the found parcel set.
Implementation of infinite scroll htmx example: https://htmx.org/examples/infinite-scroll/
Args:
request (HttpRequest): The incoming request
id (str): The geometry's id
page (int): The requested page number
Returns:
A rendered piece of HTML
"""
if page < 0:
raise AssertionError("Parcel page can not be negative")
# HTTP code 286 states that the HTMX should stop polling for updates
# https://htmx.org/docs/#polling
status_code = 286
template = "konova/includes/parcels/parcel_table_content.html"
geom = get_object_or_404(Geometry, id=id)
parcels = geom.get_underlying_parcels()
parcels = parcels.order_by("-municipal", "flr", "flrstck_zhlr", "flrstck_nnr")
rpp = 100 rpp = 100
num_all_parcels = parcels.count() from_p = rpp * (page-1)
parcels = parcels[:rpp] to_p = rpp * (page)
next_page = 1 next_page = page + 1
parcels = parcels[from_p:to_p]
if len(parcels) < rpp: if len(parcels) < rpp:
next_page = None next_page = None
context = { context = {
"num_parcels": num_all_parcels,
"parcels": parcels, "parcels": parcels,
"municipals": municipals,
"geom_id": str(id), "geom_id": str(id),
"next_page": next_page, "next_page": next_page,
} }
html = render_to_string(template, context, request) html = render_to_string(template, context, request)
return HttpResponse(html, status=status_code) return HttpResponse(html, status=status_code)
else:
return HttpResponse(None, status=404)
def get_geom_parcels_content(request: HttpRequest, id: str, page: int):
""" Getter for infinite scroll of HTMX
Returns parcels of a specific page/slice of the found parcel set.
Implementation of infinite scroll htmx example: https://htmx.org/examples/infinite-scroll/
Args:
request (HttpRequest): The incoming request
id (str): The geometry's id
page (int): The requested page number
Returns:
A rendered piece of HTML
"""
if page < 0:
raise AssertionError("Parcel page can not be negative")
# HTTP code 286 states that the HTMX should stop polling for updates
# https://htmx.org/docs/#polling
status_code = 286
template = "konova/includes/parcels/parcel_table_content.html"
geom = get_object_or_404(Geometry, id=id)
parcels = geom.get_underlying_parcels()
parcels = parcels.order_by("-municipal", "flr", "flrstck_zhlr", "flrstck_nnr")
rpp = 100
from_p = rpp * (page-1)
to_p = rpp * (page)
next_page = page + 1
parcels = parcels[from_p:to_p]
if len(parcels) < rpp:
next_page = None
context = {
"parcels": parcels,
"geom_id": str(id),
"next_page": next_page,
}
html = render_to_string(template, context, request)
return HttpResponse(html, status=status_code)

@ -5,12 +5,13 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q from django.db.models import Q
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import render from django.shortcuts import render
from django.utils import timezone from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View
from compensation.models import EcoAccount, Compensation from compensation.models import EcoAccount, Compensation
from intervention.models import Intervention from intervention.models import Intervention
@ -20,59 +21,59 @@ from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from news.models import ServerMessage from news.models import ServerMessage
@login_required class HomeView(LoginRequiredMixin, View):
@any_group_check
def home_view(request: HttpRequest):
"""
Renders the landing page
Args: @method_decorator(any_group_check)
request (HttpRequest): The used request object def get(self, request: HttpRequest):
"""
Renders the landing page
Returns: Args:
A redirect request (HttpRequest): The used request object
"""
template = "konova/home.html"
now = timezone.now()
user = request.user
user_teams = user.shared_teams
# Fetch the four newest active and published ServerMessages Returns:
msgs = ServerMessage.get_current_news()[:3] A redirect
"""
template = "konova/home.html"
user = request.user
user_teams = user.shared_teams
# First fetch all valid objects (undeleted, only newest versions) # Fetch the four newest active and published ServerMessages
interventions = Intervention.objects.filter( msgs = ServerMessage.get_current_news()[:3]
deleted=None,
)
# Then fetch only user related ones
user_interventions = interventions.filter(
Q(users__in=[user]) | Q(teams__in=user_teams)
).distinct()
# Repeat for other objects # First fetch all valid objects (undeleted, only newest versions)
comps = Compensation.objects.filter( interventions = Intervention.objects.filter(
deleted=None, deleted=None,
) )
user_comps = comps.filter( # Then fetch only user related ones
Q(intervention__users__in=[user]) | Q(intervention__teams__in=user_teams) user_interventions = interventions.filter(
).distinct() Q(users__in=[user]) | Q(teams__in=user_teams)
eco_accs = EcoAccount.objects.filter( ).distinct()
deleted=None,
)
user_ecco_accs = eco_accs.filter(
Q(users__in=[user]) | Q(teams__in=user_teams)
).distinct()
additional_context = { # Repeat for other objects
"msgs": msgs, comps = Compensation.objects.filter(
"total_intervention_count": interventions.count(), deleted=None,
"user_intervention_count": user_interventions.count(), )
"total_compensation_count": comps.count(), user_comps = comps.filter(
"user_compensation_count": user_comps.count(), Q(intervention__users__in=[user]) | Q(intervention__teams__in=user_teams)
"total_eco_count": eco_accs.count(), ).distinct()
"user_eco_count": user_ecco_accs.count(), eco_accs = EcoAccount.objects.filter(
TAB_TITLE_IDENTIFIER: _("Home"), deleted=None,
} )
context = BaseContext(request, additional_context).context user_ecco_accs = eco_accs.filter(
return render(request, template, context) Q(users__in=[user]) | Q(teams__in=user_teams)
).distinct()
additional_context = {
"msgs": msgs,
"total_intervention_count": interventions.count(),
"user_intervention_count": user_interventions.count(),
"total_compensation_count": comps.count(),
"user_compensation_count": user_comps.count(),
"total_eco_count": eco_accs.count(),
"user_eco_count": user_ecco_accs.count(),
TAB_TITLE_IDENTIFIER: _("Home"),
}
context = BaseContext(request, additional_context).context
return render(request, template, context)

@ -8,19 +8,21 @@ Created on: 19.08.22
from django.contrib.auth import logout from django.contrib.auth import logout
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import redirect from django.shortcuts import redirect
from django.views import View
from konova.sub_settings.sso_settings import SSO_SERVER_BASE from konova.sub_settings.sso_settings import SSO_SERVER_BASE
def logout_view(request: HttpRequest): class LogoutView(View):
""" def get(self, request: HttpRequest):
Logout route for ending the session manually. """
Logout route for ending the session manually.
Args: Args:
request (HttpRequest): The used request object request (HttpRequest): The used request object
Returns: Returns:
A redirect A redirect
""" """
logout(request) logout(request)
return redirect(SSO_SERVER_BASE) return redirect(SSO_SERVER_BASE)

Binary file not shown.

@ -29,21 +29,21 @@
#: konova/filters/mixins/office.py:25 konova/filters/mixins/office.py:56 #: konova/filters/mixins/office.py:25 konova/filters/mixins/office.py:56
#: konova/filters/mixins/office.py:57 konova/filters/mixins/record.py:23 #: konova/filters/mixins/office.py:57 konova/filters/mixins/record.py:23
#: konova/filters/mixins/self_created.py:24 konova/filters/mixins/share.py:23 #: konova/filters/mixins/self_created.py:24 konova/filters/mixins/share.py:23
#: konova/forms/geometry_form.py:33 konova/forms/modals/document_form.py:26 #: konova/forms/geometry_form.py:32 konova/forms/modals/document_form.py:26
#: konova/forms/modals/document_form.py:36 #: konova/forms/modals/document_form.py:36
#: konova/forms/modals/document_form.py:50 #: konova/forms/modals/document_form.py:50
#: konova/forms/modals/document_form.py:62 #: konova/forms/modals/document_form.py:62
#: konova/forms/modals/document_form.py:80 #: konova/forms/modals/document_form.py:80
#: konova/forms/modals/remove_form.py:23 #: konova/forms/modals/remove_form.py:23
#: konova/forms/modals/resubmission_form.py:22 #: konova/forms/modals/resubmission_form.py:22
#: konova/forms/modals/resubmission_form.py:38 konova/forms/remove_form.py:19 #: konova/forms/modals/resubmission_form.py:38 konova/forms/remove_form.py:25
#: user/forms/user.py:39 #: konova/tests/unit/test_forms.py:59 user/forms/user.py:39
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2023-08-29 10:29+0200\n" "POT-Creation-Date: 2023-09-08 11:30+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -96,15 +96,16 @@ msgstr "Verantwortliche Stelle"
msgid "Click for selection" msgid "Click for selection"
msgstr "Auswählen..." msgstr "Auswählen..."
#: analysis/forms.py:70 #: analysis/forms.py:70 analysis/tests/unit/test_forms.py:25
msgid "Generate report" msgid "Generate report"
msgstr "Bericht generieren" msgstr "Bericht generieren"
#: analysis/forms.py:71 #: analysis/forms.py:71 analysis/tests/unit/test_forms.py:26
msgid "Select a timespan and the desired conservation office" msgid "Select a timespan and the desired conservation office"
msgstr "Wählen Sie die Zeitspanne und die gewünschte Eintragungsstelle" msgstr "Wählen Sie die Zeitspanne und die gewünschte Eintragungsstelle"
#: analysis/forms.py:74 konova/forms/modals/base_form.py:30 #: analysis/forms.py:74 analysis/tests/unit/test_forms.py:29
#: konova/forms/modals/base_form.py:30
msgid "Continue" msgid "Continue"
msgstr "Weiter" msgstr "Weiter"
@ -231,6 +232,12 @@ msgstr "Andere Zulassungsbehörden"
msgid "Compensations" msgid "Compensations"
msgstr "Kompensationen" msgstr "Kompensationen"
#: analysis/templates/analysis/reports/includes/compensation/card_compensation.html:13
#: analysis/templates/analysis/reports/includes/eco_account/card_eco_account.html:13
#: analysis/templates/analysis/reports/includes/intervention/card_intervention.html:12
msgid "Binding date after"
msgstr "Bestandskraft- bzw. Rechtskraftdatum nach"
#: analysis/templates/analysis/reports/includes/eco_account/card_eco_account.html:11 #: analysis/templates/analysis/reports/includes/eco_account/card_eco_account.html:11
msgid "Eco-Accounts" msgid "Eco-Accounts"
msgstr "Ökokonten" msgstr "Ökokonten"
@ -345,11 +352,11 @@ msgstr "Eingriff"
msgid "Eco-account" msgid "Eco-account"
msgstr "Ökokonto" msgstr "Ökokonto"
#: analysis/templates/analysis/reports/includes/old_data/card_old_interventions.html:11 #: analysis/templates/analysis/reports/includes/old_data/card_old_data.html:11
msgid "Old interventions" msgid "Old interventions"
msgstr "Altfälle" msgstr "Altfälle"
#: analysis/templates/analysis/reports/includes/old_data/card_old_interventions.html:13 #: analysis/templates/analysis/reports/includes/old_data/card_old_data.html:13
msgid "Binding date before" msgid "Binding date before"
msgstr "Bestandskraft- bzw. Rechtskraftdatum vor" msgstr "Bestandskraft- bzw. Rechtskraftdatum vor"
@ -394,6 +401,7 @@ msgid "An explanatory name"
msgstr "Aussagekräftiger Titel" msgstr "Aussagekräftiger Titel"
#: compensation/forms/compensation.py:49 ema/forms.py:51 ema/forms.py:114 #: compensation/forms/compensation.py:49 ema/forms.py:51 ema/forms.py:114
#: ema/tests/unit/test_forms.py:31 ema/tests/unit/test_forms.py:85
msgid "Compensation XY; Location ABC" msgid "Compensation XY; Location ABC"
msgstr "Kompensation XY; Flur ABC" msgstr "Kompensation XY; Flur ABC"
@ -478,7 +486,15 @@ msgstr "Ökokonto XY; Flur ABC"
msgid "Edit Eco-Account" msgid "Edit Eco-Account"
msgstr "Ökokonto bearbeiten" msgstr "Ökokonto bearbeiten"
#: compensation/forms/eco_account.py:232 #: compensation/forms/eco_account.py:183
msgid ""
"{}m² have been deducted from this eco account so far. The given value of {} "
"would be too low."
msgstr ""
"{}n² wurden bereits von diesem Ökokonto abgebucht. Der eingegebene Wert von "
"{} wäre daher zu klein."
#: compensation/forms/eco_account.py:249
msgid "The account can not be removed, since there are still deductions." msgid "The account can not be removed, since there are still deductions."
msgstr "" msgstr ""
"Das Ökokonto kann nicht entfernt werden, da hierzu noch Abbuchungen " "Das Ökokonto kann nicht entfernt werden, da hierzu noch Abbuchungen "
@ -591,16 +607,19 @@ msgid "Insert the amount"
msgstr "Menge eingeben" msgstr "Menge eingeben"
#: compensation/forms/modals/compensation_action.py:94 #: compensation/forms/modals/compensation_action.py:94
#: compensation/tests/compensation/unit/test_forms.py:42
msgid "New action" msgid "New action"
msgstr "Neue Maßnahme" msgstr "Neue Maßnahme"
#: compensation/forms/modals/compensation_action.py:95 #: compensation/forms/modals/compensation_action.py:95
#: compensation/tests/compensation/unit/test_forms.py:43
msgid "Insert data for the new action" msgid "Insert data for the new action"
msgstr "Geben Sie die Daten der neuen Maßnahme ein" msgstr "Geben Sie die Daten der neuen Maßnahme ein"
#: compensation/forms/modals/compensation_action.py:119 #: compensation/forms/modals/compensation_action.py:119
#: compensation/templates/compensation/detail/compensation/includes/actions.html:68 #: compensation/templates/compensation/detail/compensation/includes/actions.html:68
#: compensation/templates/compensation/detail/eco_account/includes/actions.html:67 #: compensation/templates/compensation/detail/eco_account/includes/actions.html:67
#: compensation/tests/compensation/unit/test_forms.py:84
#: ema/templates/ema/detail/includes/actions.html:65 #: ema/templates/ema/detail/includes/actions.html:65
msgid "Edit action" msgid "Edit action"
msgstr "Maßnahme bearbeiten" msgstr "Maßnahme bearbeiten"
@ -634,18 +653,21 @@ msgid "Additional comment, maximum {} letters"
msgstr "Zusätzlicher Kommentar, maximal {} Zeichen" msgstr "Zusätzlicher Kommentar, maximal {} Zeichen"
#: compensation/forms/modals/deadline.py:65 #: compensation/forms/modals/deadline.py:65
#: konova/tests/unit/test_deadline.py:29
msgid "New deadline" msgid "New deadline"
msgstr "Neue Frist" msgstr "Neue Frist"
#: compensation/forms/modals/deadline.py:66 #: compensation/forms/modals/deadline.py:66
#: konova/tests/unit/test_deadline.py:30
msgid "Insert data for the new deadline" msgid "Insert data for the new deadline"
msgstr "Geben Sie die Daten der neuen Frist ein" msgstr "Geben Sie die Daten der neuen Frist ein"
#: compensation/forms/modals/deadline.py:75 #: compensation/forms/modals/deadline.py:78
#: konova/tests/unit/test_deadline.py:57
msgid "Please explain this 'other' type of deadline." msgid "Please explain this 'other' type of deadline."
msgstr "Bitte erklären Sie um welchen 'sonstigen' Termin es sich handelt." msgstr "Bitte erklären Sie um welchen 'sonstigen' Termin es sich handelt."
#: compensation/forms/modals/deadline.py:92 #: compensation/forms/modals/deadline.py:95
#: compensation/templates/compensation/detail/compensation/includes/deadlines.html:64 #: compensation/templates/compensation/detail/compensation/includes/deadlines.html:64
#: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:62 #: compensation/templates/compensation/detail/eco_account/includes/deadlines.html:62
#: ema/templates/ema/detail/includes/deadlines.html:62 #: ema/templates/ema/detail/includes/deadlines.html:62
@ -700,10 +722,12 @@ msgid "in m²"
msgstr "" msgstr ""
#: compensation/forms/modals/state.py:72 #: compensation/forms/modals/state.py:72
#: compensation/tests/compensation/unit/test_forms.py:175
msgid "New state" msgid "New state"
msgstr "Neuer Zustand" msgstr "Neuer Zustand"
#: compensation/forms/modals/state.py:73 #: compensation/forms/modals/state.py:73
#: compensation/tests/compensation/unit/test_forms.py:176
msgid "Insert data for the new state" msgid "Insert data for the new state"
msgstr "Geben Sie die Daten des neuen Zustandes ein" msgstr "Geben Sie die Daten des neuen Zustandes ein"
@ -716,6 +740,7 @@ msgstr "Objekt entfernt"
#: compensation/templates/compensation/detail/compensation/includes/states-before.html:62 #: compensation/templates/compensation/detail/compensation/includes/states-before.html:62
#: compensation/templates/compensation/detail/eco_account/includes/states-after.html:62 #: compensation/templates/compensation/detail/eco_account/includes/states-after.html:62
#: compensation/templates/compensation/detail/eco_account/includes/states-before.html:62 #: compensation/templates/compensation/detail/eco_account/includes/states-before.html:62
#: compensation/tests/compensation/unit/test_forms.py:236
#: ema/templates/ema/detail/includes/states-after.html:60 #: ema/templates/ema/detail/includes/states-after.html:60
#: ema/templates/ema/detail/includes/states-before.html:60 #: ema/templates/ema/detail/includes/states-before.html:60
msgid "Edit state" msgid "Edit state"
@ -749,21 +774,6 @@ msgstr ""
msgid "Pieces" msgid "Pieces"
msgstr "Stück" msgstr "Stück"
#: compensation/models/eco_account.py:62
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:69
msgid ""
"Deductable surface can not be smaller than the sum of already existing "
"deductions. Please contact the responsible users for the deductions!"
msgstr ""
"Es wurde bereits mehr Fläche abgebucht, als Sie nun als abbuchbar einstellen "
"wollen. Kontaktieren Sie die für die Abbuchungen verantwortlichen Nutzer!"
#: compensation/tables/compensation.py:33 compensation/tables/eco_account.py:34 #: compensation/tables/compensation.py:33 compensation/tables/eco_account.py:34
#: ema/tables.py:36 intervention/tables.py:33 #: ema/tables.py:36 intervention/tables.py:33
#: konova/filters/mixins/geo_reference.py:42 #: konova/filters/mixins/geo_reference.py:42
@ -926,6 +936,7 @@ msgstr "Öffentlicher Bericht"
#: ema/templates/ema/detail/includes/controls.html:15 #: ema/templates/ema/detail/includes/controls.html:15
#: intervention/templates/intervention/detail/includes/controls.html:15 #: intervention/templates/intervention/detail/includes/controls.html:15
#: konova/forms/modals/resubmission_form.py:51 #: konova/forms/modals/resubmission_form.py:51
#: konova/tests/unit/test_forms.py:302 konova/tests/unit/test_forms.py:316
#: templates/email/resubmission/resubmission.html:4 #: templates/email/resubmission/resubmission.html:4
msgid "Resubmission" msgid "Resubmission"
msgstr "Wiedervorlage" msgstr "Wiedervorlage"
@ -988,7 +999,7 @@ msgstr "Dokumente"
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:14 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:14
#: ema/templates/ema/detail/includes/documents.html:14 #: ema/templates/ema/detail/includes/documents.html:14
#: intervention/templates/intervention/detail/includes/documents.html:14 #: intervention/templates/intervention/detail/includes/documents.html:14
#: konova/forms/modals/document_form.py:79 #: konova/forms/modals/document_form.py:79 konova/tests/unit/test_forms.py:58
msgid "Add new document" msgid "Add new document"
msgstr "Neues Dokument hinzufügen" msgstr "Neues Dokument hinzufügen"
@ -1004,7 +1015,7 @@ msgstr "Erstellt"
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:61 #: compensation/templates/compensation/detail/eco_account/includes/documents.html:61
#: ema/templates/ema/detail/includes/documents.html:61 #: ema/templates/ema/detail/includes/documents.html:61
#: intervention/templates/intervention/detail/includes/documents.html:70 #: intervention/templates/intervention/detail/includes/documents.html:70
#: konova/forms/modals/document_form.py:141 #: konova/forms/modals/document_form.py:141 konova/tests/unit/test_forms.py:118
msgid "Edit document" msgid "Edit document"
msgstr "Dokument bearbeiten" msgstr "Dokument bearbeiten"
@ -1178,6 +1189,7 @@ msgstr "weitere Nutzer"
#: ema/templates/ema/detail/includes/controls.html:18 #: ema/templates/ema/detail/includes/controls.html:18
#: intervention/forms/modals/share.py:63 #: intervention/forms/modals/share.py:63
#: intervention/templates/intervention/detail/includes/controls.html:18 #: intervention/templates/intervention/detail/includes/controls.html:18
#: intervention/tests/unit/test_forms.py:150
msgid "Share" msgid "Share"
msgstr "Freigabe" msgstr "Freigabe"
@ -1282,14 +1294,14 @@ msgstr "Daten zu den verantwortlichen Stellen"
msgid "Compensations - Overview" msgid "Compensations - Overview"
msgstr "Kompensationen - Übersicht" msgstr "Kompensationen - Übersicht"
#: compensation/views/compensation/compensation.py:182 #: compensation/views/compensation/compensation.py:181
#: konova/utils/message_templates.py:40 #: konova/utils/message_templates.py:40
msgid "Compensation {} edited" msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet" msgstr "Kompensation {} bearbeitet"
#: compensation/views/compensation/compensation.py:197 #: compensation/views/compensation/compensation.py:196
#: compensation/views/eco_account/eco_account.py:171 ema/views/ema.py:231 #: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:231
#: intervention/views/intervention.py:253 #: intervention/views/intervention.py:252
msgid "Edit {}" msgid "Edit {}"
msgstr "Bearbeite {}" msgstr "Bearbeite {}"
@ -1307,19 +1319,19 @@ msgstr "Ökokonten - Übersicht"
msgid "Eco-Account {} added" msgid "Eco-Account {} added"
msgstr "Ökokonto {} hinzugefügt" msgstr "Ökokonto {} hinzugefügt"
#: compensation/views/eco_account/eco_account.py:156 #: compensation/views/eco_account/eco_account.py:158
msgid "Eco-Account {} edited" msgid "Eco-Account {} edited"
msgstr "Ökokonto {} bearbeitet" msgstr "Ökokonto {} bearbeitet"
#: compensation/views/eco_account/eco_account.py:285 #: compensation/views/eco_account/eco_account.py:287
msgid "Eco-account removed" msgid "Eco-account removed"
msgstr "Ökokonto entfernt" msgstr "Ökokonto entfernt"
#: ema/forms.py:42 ema/views/ema.py:102 #: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:102
msgid "New EMA" msgid "New EMA"
msgstr "Neue EMA hinzufügen" msgstr "Neue EMA hinzufügen"
#: ema/forms.py:108 #: ema/forms.py:108 ema/tests/unit/test_forms.py:81
msgid "Edit EMA" msgid "Edit EMA"
msgstr "Bearbeite EMA" msgstr "Bearbeite EMA"
@ -1418,6 +1430,7 @@ msgid "Binding on"
msgstr "Datum Bestandskraft bzw. Rechtskraft" msgstr "Datum Bestandskraft bzw. Rechtskraft"
#: intervention/forms/intervention.py:216 #: intervention/forms/intervention.py:216
#: intervention/tests/unit/test_forms.py:36
#: intervention/views/intervention.py:105 #: intervention/views/intervention.py:105
msgid "New intervention" msgid "New intervention"
msgstr "Neuer Eingriff" msgstr "Neuer Eingriff"
@ -1440,6 +1453,7 @@ msgid "Run check"
msgstr "Prüfung vornehmen" msgstr "Prüfung vornehmen"
#: intervention/forms/modals/check.py:36 konova/forms/modals/record_form.py:30 #: intervention/forms/modals/check.py:36 konova/forms/modals/record_form.py:30
#: konova/tests/unit/test_forms.py:155
msgid "" msgid ""
"I, {} {}, confirm that all necessary control steps have been performed by " "I, {} {}, confirm that all necessary control steps have been performed by "
"myself." "myself."
@ -1502,6 +1516,7 @@ msgstr "Muss kleiner als 15 Mb sein"
#: intervention/forms/modals/revocation.py:62 #: intervention/forms/modals/revocation.py:62
#: intervention/templates/intervention/detail/includes/revocation.html:18 #: intervention/templates/intervention/detail/includes/revocation.html:18
#: intervention/tests/unit/test_forms.py:234
msgid "Add revocation" msgid "Add revocation"
msgstr "Widerspruch hinzufügen" msgstr "Widerspruch hinzufügen"
@ -1543,6 +1558,7 @@ msgstr ""
"noch nicht freigegeben wurde. Geben Sie den ganzen Nutzernamen an." "noch nicht freigegeben wurde. Geben Sie den ganzen Nutzernamen an."
#: intervention/forms/modals/share.py:64 #: intervention/forms/modals/share.py:64
#: intervention/tests/unit/test_forms.py:151
msgid "Share settings for {}" msgid "Share settings for {}"
msgstr "Freigabe Einstellungen für {}" msgstr "Freigabe Einstellungen für {}"
@ -1658,11 +1674,11 @@ msgstr "Eingriffe - Übersicht"
msgid "Intervention {} added" msgid "Intervention {} added"
msgstr "Eingriff {} hinzugefügt" msgstr "Eingriff {} hinzugefügt"
#: intervention/views/intervention.py:236 #: intervention/views/intervention.py:235
msgid "Intervention {} edited" msgid "Intervention {} edited"
msgstr "Eingriff {} bearbeitet" msgstr "Eingriff {} bearbeitet"
#: intervention/views/intervention.py:278 #: intervention/views/intervention.py:277
msgid "{} removed" msgid "{} removed"
msgstr "{} entfernt" msgstr "{} entfernt"
@ -1780,12 +1796,12 @@ msgstr "Speichern"
msgid "Not editable" msgid "Not editable"
msgstr "Nicht editierbar" msgstr "Nicht editierbar"
#: konova/forms/geometry_form.py:32 konova/utils/quality.py:44 #: konova/forms/geometry_form.py:31 konova/utils/quality.py:44
#: konova/utils/quality.py:46 templates/form/collapsable/form.html:45 #: konova/utils/quality.py:46 templates/form/collapsable/form.html:45
msgid "Geometry" msgid "Geometry"
msgstr "Geometrie" msgstr "Geometrie"
#: konova/forms/geometry_form.py:101 #: konova/forms/geometry_form.py:100
msgid "Only surfaces allowed. Points or lines must be buffered." msgid "Only surfaces allowed. Points or lines must be buffered."
msgstr "" msgstr ""
"Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden." "Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden."
@ -1803,7 +1819,7 @@ msgstr "Datei"
msgid "Allowed formats: pdf, jpg, png. Max size 15 MB." msgid "Allowed formats: pdf, jpg, png. Max size 15 MB."
msgstr "Formate: pdf, jpg, png. Maximal 15 MB." msgstr "Formate: pdf, jpg, png. Maximal 15 MB."
#: konova/forms/modals/document_form.py:116 #: konova/forms/modals/document_form.py:116 konova/tests/unit/test_forms.py:95
msgid "Added document" msgid "Added document"
msgstr "Dokument hinzugefügt" msgstr "Dokument hinzugefügt"
@ -1811,32 +1827,34 @@ msgstr "Dokument hinzugefügt"
msgid "Confirm record" msgid "Confirm record"
msgstr "Verzeichnen bestätigen" msgstr "Verzeichnen bestätigen"
#: konova/forms/modals/record_form.py:29 #: konova/forms/modals/record_form.py:29 konova/tests/unit/test_forms.py:153
msgid "Record data" msgid "Record data"
msgstr "Daten verzeichnen" msgstr "Daten verzeichnen"
#: konova/forms/modals/record_form.py:36 #: konova/forms/modals/record_form.py:36 konova/tests/unit/test_forms.py:168
msgid "Confirm unrecord" msgid "Confirm unrecord"
msgstr "Entzeichnen bestätigen" msgstr "Entzeichnen bestätigen"
#: konova/forms/modals/record_form.py:37 #: konova/forms/modals/record_form.py:37 konova/tests/unit/test_forms.py:167
msgid "Unrecord data" msgid "Unrecord data"
msgstr "Daten entzeichnen" msgstr "Daten entzeichnen"
#: konova/forms/modals/record_form.py:38 #: konova/forms/modals/record_form.py:38 konova/tests/unit/test_forms.py:170
msgid "I, {} {}, confirm that this data must be unrecorded." msgid "I, {} {}, confirm that this data must be unrecorded."
msgstr "" msgstr ""
"Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen." "Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen."
#: konova/forms/modals/remove_form.py:22 konova/forms/remove_form.py:18 #: konova/forms/modals/remove_form.py:22 konova/forms/remove_form.py:24
msgid "Confirm" msgid "Confirm"
msgstr "Bestätige" msgstr "Bestätige"
#: konova/forms/modals/remove_form.py:32 konova/forms/remove_form.py:30 #: konova/forms/modals/remove_form.py:32 konova/forms/remove_form.py:36
#: konova/tests/unit/test_forms.py:209 konova/tests/unit/test_forms.py:261
msgid "Remove" msgid "Remove"
msgstr "Löschen" msgstr "Löschen"
#: konova/forms/modals/remove_form.py:33 #: konova/forms/modals/remove_form.py:33 konova/tests/unit/test_forms.py:210
#: konova/tests/unit/test_forms.py:262
msgid "Are you sure?" msgid "Are you sure?"
msgstr "Sind Sie sicher?" msgstr "Sind Sie sicher?"
@ -1845,6 +1863,7 @@ msgid "When do you want to be reminded?"
msgstr "Wann wollen Sie erinnert werden?" msgstr "Wann wollen Sie erinnert werden?"
#: konova/forms/modals/resubmission_form.py:52 #: konova/forms/modals/resubmission_form.py:52
#: konova/tests/unit/test_forms.py:303 konova/tests/unit/test_forms.py:317
msgid "Set your resubmission for this entry." msgid "Set your resubmission for this entry."
msgstr "Setzen Sie eine Wiedervorlage für diesen Eintrag." msgstr "Setzen Sie eine Wiedervorlage für diesen Eintrag."
@ -1852,7 +1871,7 @@ msgstr "Setzen Sie eine Wiedervorlage für diesen Eintrag."
msgid "The date should be in the future" msgid "The date should be in the future"
msgstr "Das Datum sollte in der Zukunft liegen" msgstr "Das Datum sollte in der Zukunft liegen"
#: konova/forms/remove_form.py:32 #: konova/forms/remove_form.py:38
msgid "You are about to remove {} {}" msgid "You are about to remove {} {}"
msgstr "Sie sind dabei {} {} zu löschen" msgstr "Sie sind dabei {} {} zu löschen"
@ -2067,8 +2086,8 @@ msgid "You need to be part of another user group."
msgstr "Hierfür müssen Sie einer anderen Nutzergruppe angehören!" msgstr "Hierfür müssen Sie einer anderen Nutzergruppe angehören!"
#: konova/utils/message_templates.py:21 #: konova/utils/message_templates.py:21
msgid "Status of Checked and Recorded reseted" msgid "Status of Checked reset"
msgstr "'Geprüft'/'Verzeichnet' wurde zurückgesetzt" msgstr "Status 'Geprüft' wurde zurückgesetzt"
#: konova/utils/message_templates.py:22 #: konova/utils/message_templates.py:22
msgid "" msgid ""
@ -2227,15 +2246,11 @@ msgstr "Dokument bearbeitet"
msgid "Edited general data" msgid "Edited general data"
msgstr "Allgemeine Daten bearbeitet" msgstr "Allgemeine Daten bearbeitet"
#: konova/utils/message_templates.py:82 #: konova/utils/message_templates.py:84
msgid "Added deadline"
msgstr "Frist/Termin hinzugefügt"
#: konova/utils/message_templates.py:85
msgid "Geometry conflict detected with {}" msgid "Geometry conflict detected with {}"
msgstr "Geometriekonflikt mit folgenden Einträgen erkannt: {}" msgstr "Geometriekonflikt mit folgenden Einträgen erkannt: {}"
#: konova/utils/message_templates.py:86 #: konova/utils/message_templates.py:85
msgid "" msgid ""
"The geometry contained more than {} vertices. It had to be simplified to " "The geometry contained more than {} vertices. It had to be simplified to "
"match the allowed limit of {} vertices." "match the allowed limit of {} vertices."
@ -2243,20 +2258,20 @@ msgstr ""
"Die Geometrie enthielt mehr als {} Eckpunkte. Sie musste vereinfacht werden " "Die Geometrie enthielt mehr als {} Eckpunkte. Sie musste vereinfacht werden "
"um die Obergrenze von {} erlaubten Eckpunkten einzuhalten." "um die Obergrenze von {} erlaubten Eckpunkten einzuhalten."
#: konova/utils/message_templates.py:89 #: konova/utils/message_templates.py:88
msgid "This intervention has {} revocations" msgid "This intervention has {} revocations"
msgstr "Dem Eingriff liegen {} Widersprüche vor" msgstr "Dem Eingriff liegen {} Widersprüche vor"
#: konova/utils/message_templates.py:92 #: konova/utils/message_templates.py:91
msgid "Checked on {} by {}" msgid "Checked on {} by {}"
msgstr "Am {} von {} geprüft worden" msgstr "Am {} von {} geprüft worden"
#: konova/utils/message_templates.py:93 #: konova/utils/message_templates.py:92
msgid "Data has changed since last check on {} by {}" msgid "Data has changed since last check on {} by {}"
msgstr "" msgstr ""
"Daten wurden nach der letzten Prüfung geändert. Letzte Prüfung am {} durch {}" "Daten wurden nach der letzten Prüfung geändert. Letzte Prüfung am {} durch {}"
#: konova/utils/message_templates.py:94 #: konova/utils/message_templates.py:93
msgid "Current data not checked yet" msgid "Current data not checked yet"
msgstr "Momentane Daten noch nicht geprüft" msgstr "Momentane Daten noch nicht geprüft"
@ -2290,7 +2305,7 @@ msgstr ""
"Dieses Datum ist unrealistisch. Geben Sie bitte das korrekte Datum ein " "Dieses Datum ist unrealistisch. Geben Sie bitte das korrekte Datum ein "
"(>1950)." "(>1950)."
#: konova/views/home.py:74 templates/navbars/navbar.html:16 #: konova/views/home.py:75 templates/navbars/navbar.html:16
msgid "Home" msgid "Home"
msgstr "Home" msgstr "Home"
@ -4607,6 +4622,24 @@ msgstr ""
msgid "Unable to connect to qpid with SASL mechanism %s" msgid "Unable to connect to qpid with SASL mechanism %s"
msgstr "" msgstr ""
#~ 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"
#~ msgid ""
#~ "Deductable surface can not be smaller than the sum of already existing "
#~ "deductions. Please contact the responsible users for the deductions!"
#~ msgstr ""
#~ "Es wurde bereits mehr Fläche abgebucht, als Sie nun als abbuchbar "
#~ "einstellen wollen. Kontaktieren Sie die für die Abbuchungen "
#~ "verantwortlichen Nutzer!"
#~ msgid "Added deadline"
#~ msgstr "Frist/Termin hinzugefügt"
#~ msgid "Change default configuration for your KSP map" #~ msgid "Change default configuration for your KSP map"
#~ msgstr "Karteneinstellungen ändern" #~ msgstr "Karteneinstellungen ändern"

@ -60,6 +60,29 @@ class User(AbstractUser):
name=ETS_GROUP name=ETS_GROUP
).exists() ).exists()
def is_default_group_only(self) -> bool:
""" Checks if the user is only part of the default group
Args:
Returns:
bool
"""
return not self.in_group(ZB_GROUP) and not self.in_group(ETS_GROUP)
def in_group(self, group: str) -> bool:
""" Checks if the user is part of a group
Args:
group (str): The group's name
Returns:
bool
"""
return self.groups.filter(
name=group
)
def send_mail_shared_access_removed(self, obj_identifier, obj_title, municipals_names): def send_mail_shared_access_removed(self, obj_identifier, obj_title, municipals_names):
""" Sends a mail to the user in case of removed shared access """ Sends a mail to the user in case of removed shared access

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 12.09.23
"""

@ -0,0 +1,286 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 12.09.23
"""
from django.core.exceptions import ObjectDoesNotExist
from django.test import RequestFactory
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from api.models import APIUserToken
from konova.tests.test_views import BaseTestCase
from user.forms.modals.team import NewTeamModalForm, EditTeamModalForm, RemoveTeamModalForm, LeaveTeamModalForm
from user.forms.user import UserNotificationForm, UserAPITokenForm
from user.models import Team, UserAction, UserNotification
class NewTeamModalFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.user
def test_init(self):
form = NewTeamModalForm(
request=self.request
)
self.assertEqual(form.form_title, str(_("Create new team")))
self.assertEqual(form.form_caption, str(_("You will become the administrator for this group by default. You do not need to add yourself to the list of members.")))
self.assertEqual(form.action_url, reverse("user:team-new"))
self.assertEqual(form.cancel_redirect, reverse("user:team-index"))
self.assertEqual(form.request, self.request)
self.assertEqual(form.user, self.request.user)
def test_is_valid(self):
invalid_data = {
"name": self.team.name,
"description": "Test description",
"members": [self.superuser.id,],
}
form = NewTeamModalForm(
invalid_data,
request=self.request
)
self.assertFalse(form.is_valid())
self.assertTrue(form.has_error("name"))
valid_data = invalid_data
valid_data["name"] = self.team.name + "_OTHER"
form = NewTeamModalForm(
invalid_data,
request=self.request
)
self.assertTrue(form.is_valid())
def test_save(self):
valid_data = {
"name": self.team.name + "_OTHER",
"description": "Test description",
"members": [self.superuser.id,],
}
form = NewTeamModalForm(
valid_data,
request=self.request
)
self.assertTrue(form.is_valid())
obj = form.save()
self.assertEqual(obj.name, valid_data["name"])
self.assertEqual(obj.description, valid_data["description"])
users = obj.users.all()
admins = obj.admins.all()
self.assertIn(self.request.user, users)
self.assertIn(self.request.user, admins)
self.assertIn(self.superuser, users)
self.assertNotIn(self.superuser, admins)
class EditTeamModalFormTestCase(NewTeamModalFormTestCase):
def test_init(self):
self.team.admins.add(self.superuser)
form = EditTeamModalForm(request=self.request, instance=self.team)
self.assertEqual(form.form_title, str(_("Edit team")))
self.assertEqual(form.action_url, reverse("user:team-edit", args=(self.team.id,)))
self.assertEqual(form.cancel_redirect, reverse("user:team-index"))
self.assertEqual(form.fields["name"].initial, self.team.name)
self.assertEqual(form.fields["description"].initial, self.team.description)
self.assertEqual(form.fields["members"].initial.count(), 1)
self.assertIn(self.superuser, form.fields["members"].initial)
self.assertEqual(form.fields["admins"].initial.count(), 1)
self.assertIn(self.superuser, form.fields["admins"].initial)
def test_is_valid(self):
data = {
"name": self.team.name,
"description": self.team.description,
"members": self.team.users.values_list("id", flat=True),
"admins": self.team.admins.values_list("id", flat=True),
}
form = EditTeamModalForm(
data,
request=self.request,
instance=self.team
)
# Error 1: Admin not in user list
self.team.users.set([self.superuser])
self.team.admins.set([self.user])
self.assertFalse(form.is_valid())
self.assertTrue(form.has_error("admins"))
# Error 2: Admin list empty
self.team.admins.set([])
self.assertFalse(form.is_valid())
self.assertTrue(form.has_error("admins"))
# Error 3: Name taken
other_team = Team.objects.create(
name=self.team.name
)
self.team.admins.set([self.superuser])
self.assertFalse(form.is_valid())
self.assertTrue(form.has_error("name"))
def test_save(self):
data = {
"name": self.team.name + "_EDITED",
"description": self.team.description + "_EDITED",
"members": [self.user.id, self.superuser.id,],
"admins": [self.user.id,],
}
form = EditTeamModalForm(
data,
request=self.request,
instance=self.team
)
self.assertTrue(form.is_valid(), msg=form.errors)
obj = form.save()
self.assertEqual(obj.name, data["name"])
self.assertEqual(obj.description, data["description"])
self.assertIn(self.user, obj.users.all())
self.assertIn(self.superuser, obj.users.all())
self.assertIn(self.user, obj.admins.all())
self.assertEqual(obj.admins.count(), 1)
self.assertEqual(obj.users.count(), 2)
class RemoveTeamModalFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.user
def test_init(self):
form = RemoveTeamModalForm(
request=self.request,
instance=self.team
)
self.assertEqual(form.form_caption, str(_("ATTENTION!\n\nRemoving the team means all members will lose their access to data, based on this team! \n\nAre you sure to remove this team?")))
self.assertEqual(form.user, self.request.user)
self.assertEqual(form.request, self.request)
def test_save(self):
data = {
"confirm": True
}
form = RemoveTeamModalForm(
data,
request=self.request,
instance=self.team
)
self.assertTrue(form.is_valid(), msg=form.errors)
form.save()
self.team.refresh_from_db()
self.assertIsNotNone(self.team.deleted)
self.assertEqual(self.team.deleted.user, self.request.user)
self.assertEqual(self.team.deleted.action, UserAction.DELETED)
class LeaveTeamModalFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.user
def test_init(self):
form = LeaveTeamModalForm(
request=self.request,
instance=self.team
)
self.assertEqual(form.form_title, str(_("Leave team")))
def test_save(self):
self.team.users.add(self.user)
data = {
"confirm": True,
}
form = LeaveTeamModalForm(
data,
request=self.request,
instance=self.team
)
self.assertTrue(form.is_valid(), msg=form.errors)
self.assertIn(self.request.user, self.team.users.all())
form.save()
self.assertNotIn(self.request.user, self.team.users.all())
class UserNotificationFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
if not UserNotification.objects.all().exists():
self.notifications = UserNotification.objects.bulk_create(
[
UserNotification(id="notification_1", name="notification_1", is_active=True),
UserNotification(id="notification_2", name="notification_2", is_active=True),
UserNotification(id="notification_3", name="notification_3", is_active=True),
UserNotification(id="notification_4", name="notification_4", is_active=True),
]
)
def test_init(self):
form = UserNotificationForm(
user=self.user
)
self.assertEqual(form.form_title, str(_("Edit notifications")))
self.assertEqual(form.form_caption, "")
self.assertEqual(form.action_url, reverse("user:notifications"))
self.assertEqual(form.cancel_redirect, reverse("user:index"))
def test_save(self):
selected_notification = UserNotification.objects.first()
data = {
"notifications": [selected_notification.id,]
}
form = UserNotificationForm(
data=data,
user=self.user
)
self.assertTrue(form.is_valid(), msg=form.errors)
self.assertEqual(self.user.notifications.count(), 0)
form.save()
self.assertEqual(self.user.notifications.count(), 1)
self.assertIn(selected_notification, self.user.notifications.all())
class UserAPITokenFormTestCase(BaseTestCase):
def test_init(self):
form = UserAPITokenForm(
instance=self.user
)
self.assertEqual(form.form_title, str(_("Create new token")))
self.assertEqual(form.form_caption, str(_("A new token needs to be validated by an administrator!")))
self.assertEqual(form.action_url, reverse("user:api-token"))
self.assertEqual(form.cancel_redirect, reverse("user:index"))
self.assertIsNone(form.fields["token"].initial)
self.assertTrue(form.fields["token"].widget.attrs["readonly"])
def test_save(self):
data = {
"token": APIUserToken().token
}
form = UserAPITokenForm(
data,
instance=self.user
)
self.assertTrue(form.is_valid(), msg=form.errors)
self.assertIsNone(self.user.api_token)
token = form.save()
self.assertEqual(self.user.api_token, token)
new_token = form.save()
self.assertEqual(self.user.api_token, new_token)
try:
token.refresh_from_db()
self.fail("Token should be deleted and not be fetchable anymore")
except ObjectDoesNotExist:
pass

@ -0,0 +1,61 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 13.09.23
"""
from konova.settings import ZB_GROUP, DEFAULT_GROUP, ETS_GROUP
from konova.tests.test_views import BaseTestCase
from user.enums import UserNotificationEnum
from user.models import UserNotification
class UserTestCase(BaseTestCase):
def test_is_notification_setting_set(self):
notification = UserNotification.objects.create(
id=UserNotificationEnum.NOTIFY_ON_DEDUCTION_CHANGES.name,
name=UserNotificationEnum.NOTIFY_ON_DEDUCTION_CHANGES.value,
)
self.assertFalse(self.user.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_DEDUCTION_CHANGES))
self.user.notifications.add(notification)
self.assertTrue(self.user.is_notification_setting_set(UserNotificationEnum.NOTIFY_ON_DEDUCTION_CHANGES))
def test_is_group_member(self):
zb_group = self.groups.get(name=ZB_GROUP)
ets_group = self.groups.get(name=ETS_GROUP)
default_group = self.groups.get(name=DEFAULT_GROUP)
self.user.groups.set([])
self.assertFalse(self.user.is_zb_user())
self.assertFalse(self.user.is_ets_user())
self.assertFalse(self.user.is_default_user())
self.user.groups.add(zb_group)
self.assertTrue(self.user.is_zb_user())
self.user.groups.add(ets_group)
self.assertTrue(self.user.is_ets_user())
self.user.groups.add(default_group)
self.assertTrue(self.user.is_default_user())
def test_get_API_token(self):
self.assertIsNone(self.user.api_token)
token = self.user.get_API_token()
self.assertIsNotNone(self.user.api_token)
self.assertEqual(self.user.api_token, token)
# Make sure the same token is returned if command is called twice
token = self.user.get_API_token()
self.assertEqual(self.user.api_token, token)
def test_shared_teams_property(self):
shared_teams = self.user.shared_teams
self.assertEqual(shared_teams.count(), 0)
self.team.users.add(self.user)
shared_teams = self.user.shared_teams
self.assertEqual(shared_teams.count(), 1)
self.assertIn(self.team, shared_teams)
Loading…
Cancel
Save