diff --git a/.gitignore b/.gitignore
index 1e56d1d8..5599d7b7 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,5 @@
# Project exclude paths
/venv/
-/.idea/
\ No newline at end of file
+/.idea/
+/.coverage
+/htmlcov/
diff --git a/analysis/settings.py b/analysis/settings.py
index 7eb022dc..de5ef149 100644
--- a/analysis/settings.py
+++ b/analysis/settings.py
@@ -9,4 +9,8 @@ Created on: 19.10.21
# Defines the date of the legal publishing of the LKompVzVo
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()
diff --git a/analysis/templates/analysis/reports/detail.html b/analysis/templates/analysis/reports/detail.html
index 31ca65c6..f553e573 100644
--- a/analysis/templates/analysis/reports/detail.html
+++ b/analysis/templates/analysis/reports/detail.html
@@ -31,6 +31,6 @@
{% include 'analysis/reports/includes/intervention/card_intervention.html' %}
{% include 'analysis/reports/includes/compensation/card_compensation.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' %}
{% endblock %}
\ No newline at end of file
diff --git a/analysis/templates/analysis/reports/includes/compensation/card_compensation.html b/analysis/templates/analysis/reports/includes/compensation/card_compensation.html
index 8fe2eec4..9a77c88f 100644
--- a/analysis/templates/analysis/reports/includes/compensation/card_compensation.html
+++ b/analysis/templates/analysis/reports/includes/compensation/card_compensation.html
@@ -10,6 +10,7 @@
{% fa5_icon 'leaf' %}
{% trans 'Compensations' %}
+ {% trans 'Binding date after' %} 16.06.2018
diff --git a/analysis/templates/analysis/reports/includes/eco_account/card_eco_account.html b/analysis/templates/analysis/reports/includes/eco_account/card_eco_account.html
index 850ac60a..f2562243 100644
--- a/analysis/templates/analysis/reports/includes/eco_account/card_eco_account.html
+++ b/analysis/templates/analysis/reports/includes/eco_account/card_eco_account.html
@@ -10,6 +10,7 @@
{% fa5_icon 'tree' %}
{% trans 'Eco-Accounts' %}
+ {% trans 'Binding date after' %} 16.06.2018
diff --git a/analysis/templates/analysis/reports/includes/intervention/card_intervention.html b/analysis/templates/analysis/reports/includes/intervention/card_intervention.html
index 6a9993a3..65897c69 100644
--- a/analysis/templates/analysis/reports/includes/intervention/card_intervention.html
+++ b/analysis/templates/analysis/reports/includes/intervention/card_intervention.html
@@ -9,6 +9,7 @@
{% fa5_icon 'pencil-ruler' %}
{% trans 'Interventions' %}
+ {% trans 'Binding date after' %} 16.06.2018
diff --git a/analysis/templates/analysis/reports/includes/old_data/card_old_interventions.html b/analysis/templates/analysis/reports/includes/old_data/card_old_data.html
similarity index 100%
rename from analysis/templates/analysis/reports/includes/old_data/card_old_interventions.html
rename to analysis/templates/analysis/reports/includes/old_data/card_old_data.html
diff --git a/analysis/tests.py b/analysis/tests.py
deleted file mode 100644
index 7ce503c2..00000000
--- a/analysis/tests.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from django.test import TestCase
-
-# Create your tests here.
diff --git a/analysis/tests/__init__.py b/analysis/tests/__init__.py
new file mode 100644
index 00000000..a34406f3
--- /dev/null
+++ b/analysis/tests/__init__.py
@@ -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
+
+"""
diff --git a/analysis/tests/unit/__init__.py b/analysis/tests/unit/__init__.py
new file mode 100644
index 00000000..a34406f3
--- /dev/null
+++ b/analysis/tests/unit/__init__.py
@@ -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
+
+"""
diff --git a/analysis/tests/unit/test_forms.py b/analysis/tests/unit/test_forms.py
new file mode 100644
index 00000000..00356313
--- /dev/null
+++ b/analysis/tests/unit/test_forms.py
@@ -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}"
+ )
diff --git a/analysis/tests/unit/test_report.py b/analysis/tests/unit/test_report.py
new file mode 100644
index 00000000..c4aa31a9
--- /dev/null
+++ b/analysis/tests/unit/test_report.py
@@ -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)
diff --git a/analysis/utils/report.py b/analysis/utils/report.py
index 9bbd7d3d..4e118fae 100644
--- a/analysis/utils/report.py
+++ b/analysis/utils/report.py
@@ -413,6 +413,7 @@ class TimespanReport:
def __init__(self, id: str, date_from: str, date_to: str):
# First fetch all eco account for this office
self.queryset = EcoAccount.objects.filter(
+ legal__registration_date__gt=LKOMPVZVO_PUBLISH_DATE,
responsible__conservation_office__id=id,
deleted=None,
created__timestamp__date__gte=date_from,
@@ -516,8 +517,8 @@ class TimespanReport:
legal__registration_date__lte=LKOMPVZVO_PUBLISH_DATE,
responsible__conservation_office__id=id,
deleted=None,
- created__timestamp__gte=date_from,
- created__timestamp__lte=date_to,
+ created__timestamp__date__gte=date_from,
+ created__timestamp__date__lte=date_to,
)
self.queryset_acc_recorded = self.queryset_acc.filter(
recorded__isnull=False,
diff --git a/api/models/token.py b/api/models/token.py
index e0ad6646..c528528c 100644
--- a/api/models/token.py
+++ b/api/models/token.py
@@ -14,7 +14,7 @@ class APIUserToken(models.Model):
valid_until = models.DateField(
blank=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(
default=False,
@@ -25,12 +25,11 @@ class APIUserToken(models.Model):
return self.token
@staticmethod
- def get_user_from_token(token: str, username: str):
+ def get_user_from_token(token: str):
""" Getter for the related user object
Args:
token (str): The used token
- username (str): The username
Returns:
user (User): Otherwise None
@@ -39,7 +38,6 @@ class APIUserToken(models.Model):
try:
token_obj = APIUserToken.objects.get(
token=token,
- user__username=username
)
if not token_obj.is_active:
raise PermissionError("Token unverified")
diff --git a/api/tests/unit/__init__.py b/api/tests/unit/__init__.py
new file mode 100644
index 00000000..5be1b4ac
--- /dev/null
+++ b/api/tests/unit/__init__.py
@@ -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
+
+"""
diff --git a/api/tests/unit/test_token.py b/api/tests/unit/test_token.py
new file mode 100644
index 00000000..0cdd9f80
--- /dev/null
+++ b/api/tests/unit/test_token.py
@@ -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)
+
diff --git a/api/tests/v1/share/test_api_sharing.py b/api/tests/v1/share/test_api_sharing.py
index 1da0ce1b..8aff6819 100644
--- a/api/tests/v1/share/test_api_sharing.py
+++ b/api/tests/v1/share/test_api_sharing.py
@@ -4,7 +4,6 @@ from django.urls import reverse
from konova.settings import DEFAULT_GROUP
from konova.tests.test_views import BaseTestCase
-from konova.utils.user_checks import is_default_group_only
class BaseAPIV1TestCase(BaseTestCase):
@@ -138,7 +137,7 @@ class APIV1SharingTestCase(BaseAPIV1TestCase):
# Give the user only default group rights
default_group = self.groups.get(name=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
self.intervention.users.set([self.superuser])
diff --git a/api/views/views.py b/api/views/views.py
index 75d764e8..72ee2779 100644
--- a/api/views/views.py
+++ b/api/views/views.py
@@ -18,7 +18,6 @@ from compensation.models import EcoAccount
from ema.models import Ema
from intervention.models import Intervention
from konova.utils.message_templates import DATA_UNSHARED
-from konova.utils.user_checks import is_default_group_only
from user.models import User, Team
@@ -53,7 +52,13 @@ class AbstractAPIView(View):
# Fetch the proper user from the given request header token
ksp_token = request.headers.get(KSP_TOKEN_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
if not self.user.is_default_user():
raise PermissionError("Default permissions required")
@@ -315,7 +320,7 @@ class AbstractModelShareAPIView(AbstractAPIView):
for team_name in new_teams:
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!
new_users_to_be_added = User.objects.filter(
username__in=new_users
diff --git a/compensation/forms/eco_account.py b/compensation/forms/eco_account.py
index 53ec207f..9e74e839 100644
--- a/compensation/forms/eco_account.py
+++ b/compensation/forms/eco_account.py
@@ -172,6 +172,23 @@ class EditEcoAccountForm(NewEcoAccountForm):
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):
with transaction.atomic():
# Fetch data from cleaned POST values
diff --git a/compensation/forms/modals/compensation_action.py b/compensation/forms/modals/compensation_action.py
index 6cd7279f..6c5e0666 100644
--- a/compensation/forms/modals/compensation_action.py
+++ b/compensation/forms/modals/compensation_action.py
@@ -93,7 +93,7 @@ class NewCompensationActionModalForm(BaseModalForm):
super().__init__(*args, **kwargs)
self.form_title = _("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],
is_archived=False,
is_leaf=True,
diff --git a/compensation/forms/modals/payment.py b/compensation/forms/modals/payment.py
index 0046838c..eed2e702 100644
--- a/compensation/forms/modals/payment.py
+++ b/compensation/forms/modals/payment.py
@@ -77,8 +77,11 @@ class NewPaymentForm(BaseModalForm):
is_valid (bool): True if valid, False otherwise
"""
super_valid = super().is_valid()
- date = self.cleaned_data["due"]
- comment = self.cleaned_data["comment"] or None
+ if not super_valid:
+ return super_valid
+
+ date = self.cleaned_data.get("due", None)
+ comment = self.cleaned_data.get("comment", None)
if not date and not comment:
# At least one needs to be set!
self.add_error(
diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py
index 0ae03fec..a32926c9 100644
--- a/compensation/models/compensation.py
+++ b/compensation/models/compensation.py
@@ -10,6 +10,7 @@ import shutil
from django.contrib import messages
from codelist.models import KonovaCode
+from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION_IDENTIFIER_LENGTH
from user.models import User, Team
from django.db import models, transaction
from django.db.models import QuerySet, Sum
@@ -298,6 +299,9 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
objects = CompensationManager()
+ identifier_length = COMPENSATION_IDENTIFIER_LENGTH
+ identifier_template = COMPENSATION_IDENTIFIER_TEMPLATE
+
def __str__(self):
return "{}".format(self.identifier)
@@ -395,7 +399,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
Returns:
users (QuerySet)
"""
- return self.intervention.users.all()
+ return self.intervention.shared_users
@property
def shared_teams(self) -> QuerySet:
@@ -404,7 +408,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
Returns:
users (QuerySet)
"""
- return self.intervention.teams.all()
+ return self.intervention.shared_teams
def get_documents(self) -> QuerySet:
""" Getter for all documents of a compensation
@@ -417,19 +421,18 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
)
return docs
- def mark_as_edited(self, user: User, request: HttpRequest = None, edit_comment: str = None, reset_recorded: bool = True):
- """ Performs internal logic for setting the recordedd/checked state of the related intervention
+ def mark_as_edited(self, user: User, request: HttpRequest = None, edit_comment: str = None):
+ """ Performs internal logic for setting the checked state of the related intervention
Args:
user (User): The performing user
request (HttpRequest): The performing request
edit_comment (str): Additional comment for the log entry
- reset_recorded (bool): Whether the record-state of the object should be reset
Returns:
"""
- self.intervention.unrecord(user, request)
+ self.intervention.set_unchecked()
action = super().mark_as_edited(user, edit_comment=edit_comment)
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
# Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path
- folder_path = self.file.path.split("/")[:-1]
- folder_path = "/".join(folder_path)
+ try:
+ folder_path = self.file.path.split("/")[:-1]
+ folder_path = "/".join(folder_path)
+ except ValueError:
+ folder_path = None
if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
diff --git a/compensation/models/eco_account.py b/compensation/models/eco_account.py
index 913937dc..fc65e500 100644
--- a/compensation/models/eco_account.py
+++ b/compensation/models/eco_account.py
@@ -9,6 +9,7 @@ import shutil
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 django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator
@@ -52,23 +53,12 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
objects = EcoAccountManager()
+ identifier_length = ECO_ACCOUNT_IDENTIFIER_LENGTH
+ identifier_template = ECO_ACCOUNT_IDENTIFIER_TEMPLATE
+
def __str__(self):
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):
if self.identifier is None or len(self.identifier) == 0:
# 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
# Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path
- folder_path = self.file.path.split("/")[:-1]
- folder_path = "/".join(folder_path)
+ try:
+ folder_path = self.file.path.split("/")[:-1]
+ folder_path = "/".join(folder_path)
+ except ValueError:
+ folder_path = None
if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
diff --git a/compensation/models/payment.py b/compensation/models/payment.py
index 6f3f4c24..ec56910d 100644
--- a/compensation/models/payment.py
+++ b/compensation/models/payment.py
@@ -10,8 +10,6 @@ from django.db import models
from intervention.models import Intervention
from konova.models import BaseResource
-from konova.utils.message_templates import PAYMENT_REMOVED
-from user.models import UserActionLogEntry
class Payment(BaseResource):
diff --git a/compensation/tests/compensation/test_workflow.py b/compensation/tests/compensation/test_workflow.py
index d14fdabd..ee520a57 100644
--- a/compensation/tests/compensation/test_workflow.py
+++ b/compensation/tests/compensation/test_workflow.py
@@ -244,6 +244,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.client_user.post(record_url, post_data)
# Check that the intervention is still not recorded
+ self.intervention.refresh_from_db()
self.assertIsNone(self.intervention.recorded)
# Now fill out the data for a compensation
diff --git a/compensation/tests/compensation/unit/__init__.py b/compensation/tests/compensation/unit/__init__.py
new file mode 100644
index 00000000..6849b3f7
--- /dev/null
+++ b/compensation/tests/compensation/unit/__init__.py
@@ -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
+
+"""
diff --git a/compensation/tests/compensation/unit/test_forms.py b/compensation/tests/compensation/unit/test_forms.py
new file mode 100644
index 00000000..333ea947
--- /dev/null
+++ b/compensation/tests/compensation/unit/test_forms.py
@@ -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)
+
diff --git a/compensation/tests/compensation/unit/test_models.py b/compensation/tests/compensation/unit/test_models.py
new file mode 100644
index 00000000..cb85a4b5
--- /dev/null
+++ b/compensation/tests/compensation/unit/test_models.py
@@ -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))
diff --git a/compensation/tests/ecoaccount/test_workflow.py b/compensation/tests/ecoaccount/test_workflow.py
index 4792120c..3bfaffeb 100644
--- a/compensation/tests/ecoaccount/test_workflow.py
+++ b/compensation/tests/ecoaccount/test_workflow.py
@@ -106,14 +106,17 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
"surface": test_deductable_surface,
"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()
+ deductions_surface = self.eco_account.get_deductions_surface()
+
check_on_elements = {
self.eco_account.title: new_title,
self.eco_account.identifier: new_identifier,
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,
}
@@ -223,7 +226,9 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.eco_account.refresh_from_db()
self.assertEqual(1, self.eco_account.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(self.eco_account.deductable_rest, self.eco_account.deductable_surface - deduction.surface)
self.assertEqual(deduction.account, self.eco_account)
diff --git a/compensation/tests/ecoaccount/unit/__init__.py b/compensation/tests/ecoaccount/unit/__init__.py
new file mode 100644
index 00000000..b457edc4
--- /dev/null
+++ b/compensation/tests/ecoaccount/unit/__init__.py
@@ -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
+
+"""
diff --git a/compensation/tests/ecoaccount/unit/test_models.py b/compensation/tests/ecoaccount/unit/test_models.py
new file mode 100644
index 00000000..8f4555ee
--- /dev/null
+++ b/compensation/tests/ecoaccount/unit/test_models.py
@@ -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
diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py
index 0a198bb1..1a091b00 100644
--- a/compensation/views/compensation/compensation.py
+++ b/compensation/views/compensation/compensation.py
@@ -25,9 +25,8 @@ from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
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
-from konova.utils.user_checks import in_group
@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)
if request.method == "POST":
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
- # about a change of the recorded/checked state
- intervention_recorded = comp.intervention.recorded is not None
- intervention_checked = comp.intervention.checked is not None
+ # Preserve state of intervention checked to determine whether the user must be informed or not
+ # about a change of the check state
+ intervention_is_checked = comp.intervention.checked is not None
# The data form takes the geom form for processing, as well as the performing user
comp = data_form.save(request.user, geom_form)
- if intervention_recorded or intervention_checked:
- messages.info(request, CHECKED_RECORDED_RESET)
+ if intervention_is_checked:
+ messages.info(request, CHECK_STATE_RESET)
messages.success(request, _("Compensation {} edited").format(comp.identifier))
if geom_form.geometry_simplified:
messages.info(
@@ -266,9 +264,9 @@ def detail_view(request: HttpRequest, id: str):
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
- "is_default_member": in_group(_user, DEFAULT_GROUP),
- "is_zb_member": in_group(_user, ZB_GROUP),
- "is_ets_member": in_group(_user, ETS_GROUP),
+ "is_default_member": _user.in_group(DEFAULT_GROUP),
+ "is_zb_member": _user.in_group(ZB_GROUP),
+ "is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": comp.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}",
"has_finished_deadlines": comp.get_finished_deadlines().exists(),
diff --git a/compensation/views/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py
index 685ddd2e..6798b647 100644
--- a/compensation/views/eco_account/eco_account.py
+++ b/compensation/views/eco_account/eco_account.py
@@ -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.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
-from konova.utils.user_checks import in_group
@login_required
@@ -150,7 +149,9 @@ def edit_view(request: HttpRequest, id: str):
data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
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
acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
@@ -242,9 +243,9 @@ def detail_view(request: HttpRequest, id: str):
"diff_states": diff_states,
"available": available_relative,
"available_total": available_total,
- "is_default_member": in_group(_user, DEFAULT_GROUP),
- "is_zb_member": in_group(_user, ZB_GROUP),
- "is_ets_member": in_group(_user, ETS_GROUP),
+ "is_default_member": _user.in_group(DEFAULT_GROUP),
+ "is_zb_member": _user.in_group(ZB_GROUP),
+ "is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": acc.get_LANIS_link(),
"deductions": deductions,
"actions": actions,
@@ -275,7 +276,7 @@ def remove_view(request: HttpRequest, id: str):
# default group user
if acc.recorded is not None or acc.deductions.exists():
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)
return redirect("compensation:acc:detail", id=id)
diff --git a/ema/forms.py b/ema/forms.py
index b1b59da1..d6b77a4d 100644
--- a/ema/forms.py
+++ b/ema/forms.py
@@ -76,7 +76,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
)
# Finally create main object
- acc = Ema.objects.create(
+ ema = Ema.objects.create(
identifier=identifier,
title=title,
responsible=responsible,
@@ -87,16 +87,16 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
)
# 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
- acc.log.add(action)
+ ema.log.add(action)
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action)
- acc.geometry = geometry
- acc.save()
- return acc
+ ema.geometry = geometry
+ ema.save()
+ return ema
class EditEmaForm(NewEmaForm):
diff --git a/ema/models/ema.py b/ema/models/ema.py
index a7172da8..5e380eef 100644
--- a/ema/models/ema.py
+++ b/ema/models/ema.py
@@ -15,6 +15,7 @@ from django.urls import reverse
from compensation.models import AbstractCompensation, PikMixin
from ema.managers import EmaManager
+from ema.settings import EMA_IDENTIFIER_LENGTH, EMA_IDENTIFIER_TEMPLATE
from ema.utils.quality import EmaQualityChecker
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE
@@ -38,6 +39,9 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, Pik
"""
objects = EmaManager()
+ identifier_length = EMA_IDENTIFIER_LENGTH
+ identifier_template = EMA_IDENTIFIER_TEMPLATE
+
def __str__(self):
return "{}".format(self.identifier)
@@ -122,7 +126,7 @@ class EmaDocument(AbstractDocument):
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.
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
# Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path
- folder_path = self.file.path.split("/")[:-1]
- folder_path = "/".join(folder_path)
+ try:
+ folder_path = self.file.path.split("/")[:-1]
+ folder_path = "/".join(folder_path)
+ except ValueError:
+ folder_path = None
if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
diff --git a/ema/settings.py b/ema/settings.py
index 0c6f5b6b..a6a124c8 100644
--- a/ema/settings.py
+++ b/ema/settings.py
@@ -6,5 +6,5 @@ Created on: 19.08.21
"""
-EMA_ACCOUNT_IDENTIFIER_LENGTH = 6
-EMA_ACCOUNT_IDENTIFIER_TEMPLATE = "EMA-{}"
\ No newline at end of file
+EMA_IDENTIFIER_LENGTH = 6
+EMA_IDENTIFIER_TEMPLATE = "EMA-{}"
\ No newline at end of file
diff --git a/ema/tests/unit/__init__.py b/ema/tests/unit/__init__.py
new file mode 100644
index 00000000..685f2583
--- /dev/null
+++ b/ema/tests/unit/__init__.py
@@ -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
+
+"""
diff --git a/ema/tests/unit/test_forms.py b/ema/tests/unit/test_forms.py
new file mode 100644
index 00000000..ff87b9fa
--- /dev/null
+++ b/ema/tests/unit/test_forms.py
@@ -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))
+
diff --git a/ema/tests/unit/test_models.py b/ema/tests/unit/test_models.py
new file mode 100644
index 00000000..b468249d
--- /dev/null
+++ b/ema/tests/unit/test_models.py
@@ -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)
+
diff --git a/ema/views/ema.py b/ema/views/ema.py
index 0296b149..2a969fc8 100644
--- a/ema/views/ema.py
+++ b/ema/views/ema.py
@@ -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.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
-from konova.utils.user_checks import in_group
@login_required
@@ -172,9 +171,9 @@ def detail_view(request: HttpRequest, id: str):
"sum_before_states": sum_before_states,
"sum_after_states": sum_after_states,
"diff_states": diff_states,
- "is_default_member": in_group(_user, DEFAULT_GROUP),
- "is_zb_member": in_group(_user, ZB_GROUP),
- "is_ets_member": in_group(_user, ETS_GROUP),
+ "is_default_member": _user.in_group(DEFAULT_GROUP),
+ "is_zb_member": _user.in_group(ZB_GROUP),
+ "is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": ema.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}",
"has_finished_deadlines": ema.get_finished_deadlines().exists(),
diff --git a/intervention/forms/modals/check.py b/intervention/forms/modals/check.py
index 4371dd2d..cc06e63b 100644
--- a/intervention/forms/modals/check.py
+++ b/intervention/forms/modals/check.py
@@ -43,6 +43,7 @@ class CheckModalForm(BaseModalForm):
"""
deductions = self.instance.deductions.all()
+ valid = True
for deduction in deductions:
checker = deduction.account.quality_check()
for msg in checker.messages:
@@ -50,8 +51,8 @@ class CheckModalForm(BaseModalForm):
"checked_comps",
f"{deduction.account.identifier}: {msg}"
)
- return checker.valid
- return True
+ valid &= checker.valid
+ return valid
def _are_comps_valid(self):
""" Performs validity checks on all types of compensations
diff --git a/intervention/forms/modals/share.py b/intervention/forms/modals/share.py
index 35c662d5..e24a13cf 100644
--- a/intervention/forms/modals/share.py
+++ b/intervention/forms/modals/share.py
@@ -12,7 +12,6 @@ from django.utils.translation import gettext_lazy as _
from intervention.inputs import TextToClipboardInput
from konova.forms.modals import BaseModalForm
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
@@ -80,7 +79,7 @@ class ShareModalForm(BaseModalForm):
teams = self.cleaned_data.get("teams", Team.objects.none())
_is_valid = True
- if is_default_group_only(self.user):
+ if self.user.is_default_group_only():
shared_users = self.instance.shared_users
shared_teams = self.instance.shared_teams
diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py
index face187f..d54a1b9d 100644
--- a/intervention/models/intervention.py
+++ b/intervention/models/intervention.py
@@ -14,7 +14,7 @@ from django.urls import reverse
from django.utils import timezone
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 user.models import User
from django.db import models, transaction
@@ -61,6 +61,9 @@ class Intervention(BaseObject,
objects = InterventionManager()
+ identifier_length = INTERVENTION_IDENTIFIER_LENGTH
+ identifier_template = INTERVENTION_IDENTIFIER_TEMPLATE
+
def __str__(self):
return f"{self.identifier} ({self.title})"
@@ -276,22 +279,20 @@ class Intervention(BaseObject,
revocation.delete()
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):
- """ In case the object or a related object changed, internal processes need to be started, such as
- unrecord and uncheck
+ def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None):
+ """ Log the edit action
+
+ If the object is checked, set it to unchecked due to the editing. Another check is needed then.
Args:
performing_user (User): The user which performed the editing action
request (HttpRequest): The used request for this action
edit_comment (str): Additional comment for the log entry
- reset_recorded (bool): Whether the record-state of the object should be reset
Returns:
"""
action = super().mark_as_edited(performing_user, edit_comment=edit_comment)
- if reset_recorded:
- self.unrecord(performing_user, request)
if self.checked:
self.set_unchecked()
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
# Make sure that the intervention folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path
- folder_path = self.file.path.split("/")[:-1]
- folder_path = "/".join(folder_path)
+ try:
+ folder_path = self.file.path.split("/")[:-1]
+ folder_path = "/".join(folder_path)
+ except ValueError:
+ folder_path = None
if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
diff --git a/intervention/tests/test_workflow.py b/intervention/tests/test_workflow.py
index d44c6d42..4bd61d92 100644
--- a/intervention/tests/test_workflow.py
+++ b/intervention/tests/test_workflow.py
@@ -400,12 +400,13 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
self.eco_account.share_with_user_list([self.superuser])
self.eco_account.save()
num_all_deducs = EcoAccountDeduction.objects.count()
+ num_acc_deducs = self.eco_account.deductions.count()
# Run the request
self.client_user.post(new_url, post_data)
# 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())
# Make sure the deduction contains the expected data
diff --git a/intervention/tests/unit/__init__.py b/intervention/tests/unit/__init__.py
new file mode 100644
index 00000000..685f2583
--- /dev/null
+++ b/intervention/tests/unit/__init__.py
@@ -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
+
+"""
diff --git a/intervention/tests/unit/test_forms.py b/intervention/tests/unit/test_forms.py
new file mode 100644
index 00000000..1c4306c2
--- /dev/null
+++ b/intervention/tests/unit/test_forms.py
@@ -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)
diff --git a/intervention/tests/unit/test_models.py b/intervention/tests/unit/test_models.py
new file mode 100644
index 00000000..acc7da2a
--- /dev/null
+++ b/intervention/tests/unit/test_models.py
@@ -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)
diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py
index a69ba4a0..e04f6281 100644
--- a/intervention/views/intervention.py
+++ b/intervention/views/intervention.py
@@ -22,8 +22,7 @@ from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
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
-from konova.utils.user_checks import in_group
+ CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
@login_required
@@ -186,9 +185,9 @@ def detail_view(request: HttpRequest, id: str):
"compensations": compensations,
"has_access": is_data_shared,
"geom_form": geom_form,
- "is_default_member": in_group(_user, DEFAULT_GROUP),
- "is_zb_member": in_group(_user, ZB_GROUP),
- "is_ets_member": in_group(_user, ETS_GROUP),
+ "is_default_member": _user.in_group(DEFAULT_GROUP),
+ "is_zb_member": _user.in_group(ZB_GROUP),
+ "is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": intervention.get_LANIS_link(),
"has_payment_without_document": has_payment_without_document,
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():
# 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
- i_rec = intervention.recorded is not None
- i_check = intervention.checked is not None
+ intervention_is_checked = intervention.checked is not None
intervention = data_form.save(request.user, geom_form)
messages.success(request, _("Intervention {} edited").format(intervention.identifier))
- if i_check or i_rec:
- messages.info(request, CHECKED_RECORDED_RESET)
+ if intervention_is_checked:
+ messages.info(request, CHECK_STATE_RESET)
if geom_form.geometry_simplified:
messages.info(
request,
diff --git a/konova/forms/geometry_form.py b/konova/forms/geometry_form.py
index ff5b0ca1..59664fdc 100644
--- a/konova/forms/geometry_form.py
+++ b/konova/forms/geometry_form.py
@@ -8,10 +8,10 @@ Created on: 15.08.22
import json
from django.contrib.gis import gdal
-from django.contrib.gis.forms import MultiPolygonField
from django.contrib.gis.geos import MultiPolygon, Polygon
from django.contrib.gis.geos.prototypes.io import WKTWriter
from django.utils.translation import gettext_lazy as _
+from django.forms import JSONField
from konova.forms.base_form import BaseForm
from konova.models import Geometry
@@ -27,8 +27,7 @@ class SimpleGeomForm(BaseForm):
"""
read_only = True
geometry_simplified = False
- geom = MultiPolygonField(
- srid=DEFAULT_SRID_RLP,
+ geom = JSONField(
label=_("Geometry"),
help_text=_(""),
label_suffix="",
diff --git a/konova/forms/remove_form.py b/konova/forms/remove_form.py
index d5c884a6..5d08ca22 100644
--- a/konova/forms/remove_form.py
+++ b/konova/forms/remove_form.py
@@ -14,6 +14,12 @@ from user.models import UserActionLogEntry, User
class RemoveForm(BaseForm):
+ """ DEPRECATED
+
+ NOT USED IN ANY PLACE.
+ CAN BE DELETED AT SOME POINT.
+
+ """
check = forms.BooleanField(
label=_("Confirm"),
label_suffix=_(""),
diff --git a/konova/models/geometry.py b/konova/models/geometry.py
index 870b7c60..81ba2d77 100644
--- a/konova/models/geometry.py
+++ b/konova/models/geometry.py
@@ -317,7 +317,7 @@ class Geometry(BaseResource):
"""
geom = self.geom
if geom.srid != srid:
- geom.transform(ct=srid)
+ geom.transform(srid)
polygons = [p for p in geom]
geojson = {
"type": "FeatureCollection",
diff --git a/konova/models/object.py b/konova/models/object.py
index 5491e54e..038b47f9 100644
--- a/konova/models/object.py
+++ b/konova/models/object.py
@@ -23,13 +23,9 @@ from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest
from django.utils.timezone import now
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.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):
@@ -143,6 +139,9 @@ class BaseObject(BaseResource, DeletableObjectMixin):
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)
+ identifier_length = 6 # Fallback - specified in inheriting classes
+ identifier_template = "UNBEKANNT-{}" # Fallback - specified in inheriting classes
+
class Meta:
abstract = True
@@ -193,32 +192,8 @@ class BaseObject(BaseResource, DeletableObjectMixin):
Returns:
str
"""
- from compensation.models import Compensation, EcoAccount
- from intervention.models import Intervention
- 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)
+ id_len = self.identifier_length
+ id_template = self.identifier_template
_now = now()
curr_month = _now.month
@@ -229,13 +204,13 @@ class BaseObject(BaseResource, DeletableObjectMixin):
curr_month = str(curr_month)
curr_year = str(_now.year)
rand_str = generate_random_string(
- length=definitions[self.__class__]["length"],
+ length=id_len,
use_numbers=True,
use_letters_lc=False,
use_letters_uc=True,
)
_str = "{}{}-{}".format(curr_month, curr_year, rand_str)
- return definitions[self.__class__]["template"].format(_str)
+ return id_template.format(_str)
@abstractmethod
def get_detail_url(self):
@@ -319,27 +294,6 @@ class RecordableObjectMixin(models.Model):
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
def is_ready_for_publish(self) -> bool:
""" Check for all needed publishing-constraints on the data
@@ -374,7 +328,7 @@ class CheckableObjectMixin(models.Model):
abstract = True
def set_unchecked(self) -> None:
- """ Perform unrecording
+ """ Perform unchecking
Args:
@@ -384,7 +338,7 @@ class CheckableObjectMixin(models.Model):
if not self.checked:
# Nothing to do
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!
self.checked = None
self.save()
@@ -686,12 +640,11 @@ class ShareableObjectMixin(models.Model):
Returns:
"""
- from konova.utils.user_checks import is_default_group_only
users = self.shared_users
cleaned_users = []
default_users = []
for user in users:
- if not is_default_group_only(user):
+ if not user.is_default_group_only():
cleaned_users.append(user)
else:
default_users.append(user)
diff --git a/konova/sub_settings/lanis_settings.py b/konova/sub_settings/lanis_settings.py
index ae71866d..58a28568 100644
--- a/konova/sub_settings/lanis_settings.py
+++ b/konova/sub_settings/lanis_settings.py
@@ -15,7 +15,7 @@ DEFAULT_SRID_RLP = 25832
# Needed to redirect to LANIS
## 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.
LANIS_ZOOM_LUT = {
1000000000: 6,
diff --git a/konova/tests/test_geometries.py b/konova/tests/test_geometries.py
index cf518853..047e285d 100644
--- a/konova/tests/test_geometries.py
+++ b/konova/tests/test_geometries.py
@@ -5,9 +5,12 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 15.12.21
"""
+import json
+
from django.contrib.gis.db.models.functions import Translate
from konova.models import Geometry, GeometryConflict
+from konova.sub_settings.lanis_settings import DEFAULT_SRID
from konova.tests.test_views import BaseTestCase
from konova.utils.schneider.fetcher import ParcelFetcher
@@ -74,3 +77,66 @@ class GeometryTestCase(BaseTestCase):
fetcher = ParcelFetcher(geometry=self.geom_1)
features = fetcher.get_parcels()
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)
+
diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py
index a78116fc..c540e785 100644
--- a/konova/tests/test_views.py
+++ b/konova/tests/test_views.py
@@ -8,7 +8,7 @@ Created on: 26.10.21
import datetime
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 konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from user.models import User, Team
@@ -157,14 +157,17 @@ class BaseTestCase(TestCase):
intervention.generate_access_token(make_unique=True)
return intervention
- def create_dummy_compensation(self):
+ def create_dummy_compensation(self, interv: Intervention=None):
""" Creates a compensation which can be used for tests
Returns:
"""
- if self.intervention is None:
- self.intervention = self.create_dummy_intervention()
+ if not interv:
+ if self.intervention is None:
+ interv = self.create_dummy_intervention()
+ else:
+ interv = self.intervention
# Create dummy data
# Create log entry
action = UserActionLogEntry.get_created_action(self.superuser)
@@ -173,7 +176,7 @@ class BaseTestCase(TestCase):
compensation = Compensation.objects.create(
identifier="TEST",
title="Test_title",
- intervention=self.intervention,
+ intervention=interv,
created=action,
geometry=geometry,
comment="Test",
@@ -196,9 +199,11 @@ class BaseTestCase(TestCase):
handler = self.handler
responsible_data.handler = handler
responsible_data.save()
+
+ identifier = EcoAccount().generate_new_identifier()
# Finally create main object, holding the other objects
eco_account = EcoAccount.objects.create(
- identifier="TEST",
+ identifier=identifier,
title="Test_title",
deductable_surface=500,
legal=lega_data,
@@ -234,10 +239,15 @@ class BaseTestCase(TestCase):
)
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(
- account=self.create_dummy_eco_account(),
- intervention=self.create_dummy_intervention(),
+ account=acc,
+ intervention=interv,
surface=100,
)
@@ -270,15 +280,17 @@ class BaseTestCase(TestCase):
Returns:
"""
- codes = KonovaCode.objects.bulk_create([
- KonovaCode(id=1, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test1"),
- 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"),
- ])
+ codes = KonovaCode.objects.all()
+ if codes.count() == 0:
+ codes = KonovaCode.objects.bulk_create([
+ KonovaCode(id=1, is_selectable=True, is_archived=False, is_leaf=True, long_name="Test1"),
+ 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
- def create_dummy_team(self):
+ def create_dummy_team(self, name: str = None):
""" Creates a dummy team
Returns:
@@ -287,8 +299,11 @@ class BaseTestCase(TestCase):
if self.superuser is None:
self.create_users()
+ if not name:
+ name = "Testteam"
+
team = Team.objects.get_or_create(
- name="Testteam",
+ name=name,
description="Testdescription",
)[0]
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.srid = 4326
- polygon = polygon.transform(DEFAULT_SRID_RLP, clone=True)
+ polygon.transform(DEFAULT_SRID_RLP)
return MultiPolygon(polygon, srid=DEFAULT_SRID_RLP)
def create_geojson(self, geometry):
@@ -408,6 +423,19 @@ class BaseTestCase(TestCase):
codelist.codes.add(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):
""" Adds all required (dummy) data to an Ema
diff --git a/konova/tests/unit/test_forms.py b/konova/tests/unit/test_forms.py
new file mode 100644
index 00000000..3cbb4371
--- /dev/null
+++ b/konova/tests/unit/test_forms.py
@@ -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)
diff --git a/konova/tests/unit/test_models.py b/konova/tests/unit/test_models.py
new file mode 100644
index 00000000..2ec7f0f3
--- /dev/null
+++ b/konova/tests/unit/test_models.py
@@ -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)
diff --git a/konova/urls.py b/konova/urls.py
index a3672d47..260251ad 100644
--- a/konova/urls.py
+++ b/konova/urls.py
@@ -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.sso.sso import KonovaSSOClient
-from konova.views.logout import logout_view
-from konova.views.geometry import get_geom_parcels, get_geom_parcels_content
-from konova.views.home import home_view
+from konova.views.logout import LogoutView
+from konova.views.geometry import GeomParcelsView, GeomParcelsContentView
+from konova.views.home import HomeView
from konova.views.map_proxy import ClientProxyParcelSearch, ClientProxyParcelWFS
sso_client = KonovaSSOClient(SSO_SERVER, SSO_PUBLIC_KEY, SSO_PRIVATE_KEY)
urlpatterns = [
path('admin/', admin.site.urls),
path('login/', include(sso_client.get_urls())),
- path('logout/', logout_view, name="logout"),
- path('', home_view, name="home"),
+ path('logout/', LogoutView.as_view(), name="logout"),
+ path('', HomeView.as_view(), name="home"),
path('intervention/', include("intervention.urls")),
path('compensation/', include("compensation.urls")),
path('ema/', include("ema.urls")),
@@ -38,8 +38,8 @@ urlpatterns = [
path('cl/', include("codelist.urls")),
path('analysis/', include("analysis.urls")),
path('api/', include("api.urls")),
- path('geom//parcels/', get_geom_parcels, name="geometry-parcels"),
- path('geom//parcels/', get_geom_parcels_content, name="geometry-parcels-content"),
+ path('geom//parcels/', GeomParcelsView.as_view(), name="geometry-parcels"),
+ path('geom//parcels/', GeomParcelsContentView.as_view(), name="geometry-parcels-content"),
path('client/proxy', ClientProxyParcelSearch.as_view(), name="client-proxy-search"),
path('client/proxy/wfs', ClientProxyParcelWFS.as_view(), name="client-proxy-wfs"),
]
@@ -50,4 +50,4 @@ if DEBUG:
]
handler404 = "konova.views.error.get_404_view"
-handler500 = "konova.views.error.get_500_view"
\ No newline at end of file
+handler500 = "konova.views.error.get_500_view"
diff --git a/konova/utils/message_templates.py b/konova/utils/message_templates.py
index 6dcaa335..b36e2a0a 100644
--- a/konova/utils/message_templates.py
+++ b/konova/utils/message_templates.py
@@ -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")
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.")
-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.")
# SHARE
diff --git a/konova/utils/user_checks.py b/konova/utils/user_checks.py
deleted file mode 100644
index b4f65271..00000000
--- a/konova/utils/user_checks.py
+++ /dev/null
@@ -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)
\ No newline at end of file
diff --git a/konova/views/geometry.py b/konova/views/geometry.py
index f22b9c15..bf34e612 100644
--- a/konova/views/geometry.py
+++ b/konova/views/geometry.py
@@ -5,104 +5,110 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
+from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.gis.geos import MultiPolygon
from django.http import HttpResponse, HttpRequest
from django.shortcuts import get_object_or_404
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
-def get_geom_parcels(request: HttpRequest, id: str):
- """ Getter for HTMX
+class GeomParcelsView(LoginRequiredMixin, View):
- Returns all parcels of the requested geometry rendered into a simple HTML table
+ def get(self, request: HttpRequest, id: str):
+ """ Getter for HTMX
- Args:
- request (HttpRequest): The incoming request
- id (str): The geometry's id
+ Returns all parcels of the requested geometry rendered into a simple HTML table
- Returns:
- A rendered piece of HTML
- """
- # 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_frame.html"
- geom = get_object_or_404(Geometry, id=id)
- parcels = geom.get_underlying_parcels()
- geos_geom = geom.geom or MultiPolygon(srid=DEFAULT_SRID_RLP)
+ Args:
+ request (HttpRequest): The incoming request
+ id (str): The geometry's id
- geometry_exists = not geos_geom.empty
- parcels_are_currently_calculated = geometry_exists and geos_geom.area > 0 and len(parcels) == 0
- parcels_available = len(parcels) > 0
+ Returns:
+ A rendered piece of HTML
+ """
+ # 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_frame.html"
+ geom = get_object_or_404(Geometry, id=id)
+ parcels = geom.get_underlying_parcels()
+ geos_geom = geom.geom or MultiPolygon(srid=DEFAULT_SRID_RLP)
- if parcels_are_currently_calculated:
- # Parcels are being calculated right now. Change the status code, so polling stays active for fetching
- # resutls after the calculation
- status_code = 200
+ geometry_exists = not geos_geom.empty
+ parcels_are_currently_calculated = geometry_exists and geos_geom.area > 0 and len(parcels) == 0
+ parcels_available = len(parcels) > 0
- if parcels_available or not geometry_exists:
- municipals = geom.get_underlying_municipals(parcels)
+ if parcels_are_currently_calculated:
+ # Parcels are being calculated right now. Change the status code, so polling stays active for fetching
+ # resutls after the calculation
+ status_code = 200
+ 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
- num_all_parcels = parcels.count()
- parcels = parcels[:rpp]
- next_page = 1
+ 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 = {
- "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)
-
-
-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)
diff --git a/konova/views/home.py b/konova/views/home.py
index 743ef32a..5253bbfe 100644
--- a/konova/views/home.py
+++ b/konova/views/home.py
@@ -5,12 +5,13 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
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.http import HttpRequest
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.views import View
from compensation.models import EcoAccount, Compensation
from intervention.models import Intervention
@@ -20,59 +21,59 @@ from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from news.models import ServerMessage
-@login_required
-@any_group_check
-def home_view(request: HttpRequest):
- """
- Renders the landing page
+class HomeView(LoginRequiredMixin, View):
- Args:
- request (HttpRequest): The used request object
+ @method_decorator(any_group_check)
+ def get(self, request: HttpRequest):
+ """
+ Renders the landing page
- Returns:
- A redirect
- """
- template = "konova/home.html"
- now = timezone.now()
- user = request.user
- user_teams = user.shared_teams
+ Args:
+ request (HttpRequest): The used request object
- # Fetch the four newest active and published ServerMessages
- msgs = ServerMessage.get_current_news()[:3]
+ Returns:
+ A redirect
+ """
+ template = "konova/home.html"
+ user = request.user
+ user_teams = user.shared_teams
- # First fetch all valid objects (undeleted, only newest versions)
- interventions = Intervention.objects.filter(
- deleted=None,
- )
- # Then fetch only user related ones
- user_interventions = interventions.filter(
- Q(users__in=[user]) | Q(teams__in=user_teams)
- ).distinct()
+ # Fetch the four newest active and published ServerMessages
+ msgs = ServerMessage.get_current_news()[:3]
- # Repeat for other objects
- comps = Compensation.objects.filter(
- deleted=None,
- )
- user_comps = comps.filter(
- Q(intervention__users__in=[user]) | Q(intervention__teams__in=user_teams)
- ).distinct()
- eco_accs = EcoAccount.objects.filter(
- deleted=None,
- )
- user_ecco_accs = eco_accs.filter(
- Q(users__in=[user]) | Q(teams__in=user_teams)
- ).distinct()
+ # First fetch all valid objects (undeleted, only newest versions)
+ interventions = Intervention.objects.filter(
+ deleted=None,
+ )
+ # Then fetch only user related ones
+ user_interventions = interventions.filter(
+ 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)
+ # Repeat for other objects
+ comps = Compensation.objects.filter(
+ deleted=None,
+ )
+ user_comps = comps.filter(
+ Q(intervention__users__in=[user]) | Q(intervention__teams__in=user_teams)
+ ).distinct()
+ eco_accs = EcoAccount.objects.filter(
+ deleted=None,
+ )
+ user_ecco_accs = eco_accs.filter(
+ 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)
diff --git a/konova/views/logout.py b/konova/views/logout.py
index 943673d0..fe4d0db4 100644
--- a/konova/views/logout.py
+++ b/konova/views/logout.py
@@ -8,19 +8,21 @@ Created on: 19.08.22
from django.contrib.auth import logout
from django.http import HttpRequest
from django.shortcuts import redirect
+from django.views import View
from konova.sub_settings.sso_settings import SSO_SERVER_BASE
-def logout_view(request: HttpRequest):
- """
- Logout route for ending the session manually.
+class LogoutView(View):
+ def get(self, request: HttpRequest):
+ """
+ Logout route for ending the session manually.
- Args:
- request (HttpRequest): The used request object
+ Args:
+ request (HttpRequest): The used request object
- Returns:
- A redirect
- """
- logout(request)
- return redirect(SSO_SERVER_BASE)
+ Returns:
+ A redirect
+ """
+ logout(request)
+ return redirect(SSO_SERVER_BASE)
diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo
index 2f155e47..cbfcbda4 100644
Binary files a/locale/de/LC_MESSAGES/django.mo and b/locale/de/LC_MESSAGES/django.mo differ
diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po
index 7c17cb7f..4eef6ffc 100644
--- a/locale/de/LC_MESSAGES/django.po
+++ b/locale/de/LC_MESSAGES/django.po
@@ -29,21 +29,21 @@
#: 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/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:50
#: konova/forms/modals/document_form.py:62
#: konova/forms/modals/document_form.py:80
#: konova/forms/modals/remove_form.py:23
#: konova/forms/modals/resubmission_form.py:22
-#: konova/forms/modals/resubmission_form.py:38 konova/forms/remove_form.py:19
-#: user/forms/user.py:39
+#: konova/forms/modals/resubmission_form.py:38 konova/forms/remove_form.py:25
+#: konova/tests/unit/test_forms.py:59 user/forms/user.py:39
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\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"
"Last-Translator: FULL NAME \n"
"Language-Team: LANGUAGE \n"
@@ -96,15 +96,16 @@ msgstr "Verantwortliche Stelle"
msgid "Click for selection"
msgstr "Auswählen..."
-#: analysis/forms.py:70
+#: analysis/forms.py:70 analysis/tests/unit/test_forms.py:25
msgid "Generate report"
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"
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"
msgstr "Weiter"
@@ -231,6 +232,12 @@ msgstr "Andere Zulassungsbehörden"
msgid "Compensations"
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
msgid "Eco-Accounts"
msgstr "Ökokonten"
@@ -345,11 +352,11 @@ msgstr "Eingriff"
msgid "Eco-account"
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"
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"
msgstr "Bestandskraft- bzw. Rechtskraftdatum vor"
@@ -394,6 +401,7 @@ msgid "An explanatory name"
msgstr "Aussagekräftiger Titel"
#: 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"
msgstr "Kompensation XY; Flur ABC"
@@ -478,7 +486,15 @@ msgstr "Ökokonto XY; Flur ABC"
msgid "Edit Eco-Account"
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."
msgstr ""
"Das Ökokonto kann nicht entfernt werden, da hierzu noch Abbuchungen "
@@ -591,16 +607,19 @@ msgid "Insert the amount"
msgstr "Menge eingeben"
#: compensation/forms/modals/compensation_action.py:94
+#: compensation/tests/compensation/unit/test_forms.py:42
msgid "New action"
msgstr "Neue Maßnahme"
#: compensation/forms/modals/compensation_action.py:95
+#: compensation/tests/compensation/unit/test_forms.py:43
msgid "Insert data for the new action"
msgstr "Geben Sie die Daten der neuen Maßnahme ein"
#: compensation/forms/modals/compensation_action.py:119
#: compensation/templates/compensation/detail/compensation/includes/actions.html:68
#: 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
msgid "Edit action"
msgstr "Maßnahme bearbeiten"
@@ -634,18 +653,21 @@ msgid "Additional comment, maximum {} letters"
msgstr "Zusätzlicher Kommentar, maximal {} Zeichen"
#: compensation/forms/modals/deadline.py:65
+#: konova/tests/unit/test_deadline.py:29
msgid "New deadline"
msgstr "Neue Frist"
#: compensation/forms/modals/deadline.py:66
+#: konova/tests/unit/test_deadline.py:30
msgid "Insert data for the new deadline"
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."
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/eco_account/includes/deadlines.html:62
#: ema/templates/ema/detail/includes/deadlines.html:62
@@ -700,10 +722,12 @@ msgid "in m²"
msgstr ""
#: compensation/forms/modals/state.py:72
+#: compensation/tests/compensation/unit/test_forms.py:175
msgid "New state"
msgstr "Neuer Zustand"
#: compensation/forms/modals/state.py:73
+#: compensation/tests/compensation/unit/test_forms.py:176
msgid "Insert data for the new state"
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/eco_account/includes/states-after.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-before.html:60
msgid "Edit state"
@@ -749,21 +774,6 @@ msgstr ""
msgid "Pieces"
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
#: ema/tables.py:36 intervention/tables.py:33
#: konova/filters/mixins/geo_reference.py:42
@@ -926,6 +936,7 @@ msgstr "Öffentlicher Bericht"
#: ema/templates/ema/detail/includes/controls.html:15
#: intervention/templates/intervention/detail/includes/controls.html:15
#: 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
msgid "Resubmission"
msgstr "Wiedervorlage"
@@ -988,7 +999,7 @@ msgstr "Dokumente"
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:14
#: ema/templates/ema/detail/includes/documents.html:14
#: intervention/templates/intervention/detail/includes/documents.html:14
-#: konova/forms/modals/document_form.py:79
+#: konova/forms/modals/document_form.py:79 konova/tests/unit/test_forms.py:58
msgid "Add new document"
msgstr "Neues Dokument hinzufügen"
@@ -1004,7 +1015,7 @@ msgstr "Erstellt"
#: compensation/templates/compensation/detail/eco_account/includes/documents.html:61
#: ema/templates/ema/detail/includes/documents.html:61
#: intervention/templates/intervention/detail/includes/documents.html: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"
msgstr "Dokument bearbeiten"
@@ -1178,6 +1189,7 @@ msgstr "weitere Nutzer"
#: ema/templates/ema/detail/includes/controls.html:18
#: intervention/forms/modals/share.py:63
#: intervention/templates/intervention/detail/includes/controls.html:18
+#: intervention/tests/unit/test_forms.py:150
msgid "Share"
msgstr "Freigabe"
@@ -1282,14 +1294,14 @@ msgstr "Daten zu den verantwortlichen Stellen"
msgid "Compensations - Overview"
msgstr "Kompensationen - Übersicht"
-#: compensation/views/compensation/compensation.py:182
+#: compensation/views/compensation/compensation.py:181
#: konova/utils/message_templates.py:40
msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet"
-#: compensation/views/compensation/compensation.py:197
-#: compensation/views/eco_account/eco_account.py:171 ema/views/ema.py:231
-#: intervention/views/intervention.py:253
+#: compensation/views/compensation/compensation.py:196
+#: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:231
+#: intervention/views/intervention.py:252
msgid "Edit {}"
msgstr "Bearbeite {}"
@@ -1307,19 +1319,19 @@ msgstr "Ökokonten - Übersicht"
msgid "Eco-Account {} added"
msgstr "Ökokonto {} hinzugefügt"
-#: compensation/views/eco_account/eco_account.py:156
+#: compensation/views/eco_account/eco_account.py:158
msgid "Eco-Account {} edited"
msgstr "Ökokonto {} bearbeitet"
-#: compensation/views/eco_account/eco_account.py:285
+#: compensation/views/eco_account/eco_account.py:287
msgid "Eco-account removed"
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"
msgstr "Neue EMA hinzufügen"
-#: ema/forms.py:108
+#: ema/forms.py:108 ema/tests/unit/test_forms.py:81
msgid "Edit EMA"
msgstr "Bearbeite EMA"
@@ -1418,6 +1430,7 @@ msgid "Binding on"
msgstr "Datum Bestandskraft bzw. Rechtskraft"
#: intervention/forms/intervention.py:216
+#: intervention/tests/unit/test_forms.py:36
#: intervention/views/intervention.py:105
msgid "New intervention"
msgstr "Neuer Eingriff"
@@ -1440,6 +1453,7 @@ msgid "Run check"
msgstr "Prüfung vornehmen"
#: intervention/forms/modals/check.py:36 konova/forms/modals/record_form.py:30
+#: konova/tests/unit/test_forms.py:155
msgid ""
"I, {} {}, confirm that all necessary control steps have been performed by "
"myself."
@@ -1502,6 +1516,7 @@ msgstr "Muss kleiner als 15 Mb sein"
#: intervention/forms/modals/revocation.py:62
#: intervention/templates/intervention/detail/includes/revocation.html:18
+#: intervention/tests/unit/test_forms.py:234
msgid "Add revocation"
msgstr "Widerspruch hinzufügen"
@@ -1543,6 +1558,7 @@ msgstr ""
"noch nicht freigegeben wurde. Geben Sie den ganzen Nutzernamen an."
#: intervention/forms/modals/share.py:64
+#: intervention/tests/unit/test_forms.py:151
msgid "Share settings for {}"
msgstr "Freigabe Einstellungen für {}"
@@ -1658,11 +1674,11 @@ msgstr "Eingriffe - Übersicht"
msgid "Intervention {} added"
msgstr "Eingriff {} hinzugefügt"
-#: intervention/views/intervention.py:236
+#: intervention/views/intervention.py:235
msgid "Intervention {} edited"
msgstr "Eingriff {} bearbeitet"
-#: intervention/views/intervention.py:278
+#: intervention/views/intervention.py:277
msgid "{} removed"
msgstr "{} entfernt"
@@ -1780,12 +1796,12 @@ msgstr "Speichern"
msgid "Not editable"
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
msgid "Geometry"
msgstr "Geometrie"
-#: konova/forms/geometry_form.py:101
+#: konova/forms/geometry_form.py:100
msgid "Only surfaces allowed. Points or lines must be buffered."
msgstr ""
"Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden."
@@ -1803,7 +1819,7 @@ msgstr "Datei"
msgid "Allowed formats: pdf, jpg, png. Max size 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"
msgstr "Dokument hinzugefügt"
@@ -1811,32 +1827,34 @@ msgstr "Dokument hinzugefügt"
msgid "Confirm record"
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"
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"
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"
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."
msgstr ""
"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"
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"
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?"
msgstr "Sind Sie sicher?"
@@ -1845,6 +1863,7 @@ msgid "When do you want to be reminded?"
msgstr "Wann wollen Sie erinnert werden?"
#: 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."
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"
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 {} {}"
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!"
#: konova/utils/message_templates.py:21
-msgid "Status of Checked and Recorded reseted"
-msgstr "'Geprüft'/'Verzeichnet' wurde zurückgesetzt"
+msgid "Status of Checked reset"
+msgstr "Status 'Geprüft' wurde zurückgesetzt"
#: konova/utils/message_templates.py:22
msgid ""
@@ -2227,15 +2246,11 @@ msgstr "Dokument bearbeitet"
msgid "Edited general data"
msgstr "Allgemeine Daten bearbeitet"
-#: konova/utils/message_templates.py:82
-msgid "Added deadline"
-msgstr "Frist/Termin hinzugefügt"
-
-#: konova/utils/message_templates.py:85
+#: konova/utils/message_templates.py:84
msgid "Geometry conflict detected with {}"
msgstr "Geometriekonflikt mit folgenden Einträgen erkannt: {}"
-#: konova/utils/message_templates.py:86
+#: konova/utils/message_templates.py:85
msgid ""
"The geometry contained more than {} vertices. It had to be simplified to "
"match the allowed limit of {} vertices."
@@ -2243,20 +2258,20 @@ msgstr ""
"Die Geometrie enthielt mehr als {} Eckpunkte. Sie musste vereinfacht werden "
"um die Obergrenze von {} erlaubten Eckpunkten einzuhalten."
-#: konova/utils/message_templates.py:89
+#: konova/utils/message_templates.py:88
msgid "This intervention has {} revocations"
msgstr "Dem Eingriff liegen {} Widersprüche vor"
-#: konova/utils/message_templates.py:92
+#: konova/utils/message_templates.py:91
msgid "Checked on {} by {}"
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 {}"
msgstr ""
"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"
msgstr "Momentane Daten noch nicht geprüft"
@@ -2290,7 +2305,7 @@ msgstr ""
"Dieses Datum ist unrealistisch. Geben Sie bitte das korrekte Datum ein "
"(>1950)."
-#: konova/views/home.py:74 templates/navbars/navbar.html:16
+#: konova/views/home.py:75 templates/navbars/navbar.html:16
msgid "Home"
msgstr "Home"
@@ -4607,6 +4622,24 @@ msgstr ""
msgid "Unable to connect to qpid with SASL mechanism %s"
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"
#~ msgstr "Karteneinstellungen ändern"
diff --git a/user/models/user.py b/user/models/user.py
index ad7a3eca..c01dbc48 100644
--- a/user/models/user.py
+++ b/user/models/user.py
@@ -60,6 +60,29 @@ class User(AbstractUser):
name=ETS_GROUP
).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):
""" Sends a mail to the user in case of removed shared access
diff --git a/user/tests/unit/__init__.py b/user/tests/unit/__init__.py
new file mode 100644
index 00000000..4301bfcb
--- /dev/null
+++ b/user/tests/unit/__init__.py
@@ -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
+
+"""
diff --git a/user/tests/unit/test_forms.py b/user/tests/unit/test_forms.py
new file mode 100644
index 00000000..48ecd5d9
--- /dev/null
+++ b/user/tests/unit/test_forms.py
@@ -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
diff --git a/user/tests/unit/test_models.py b/user/tests/unit/test_models.py
new file mode 100644
index 00000000..4c9338a5
--- /dev/null
+++ b/user/tests/unit/test_models.py
@@ -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)
+