diff --git a/.gitignore b/.gitignore index 1e56d1d..5599d7b 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 7eb022d..de5ef14 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 31ca65c..f553e57 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 8fe2eec..9a77c88 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 850ac60..f256224 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 6a9993a..65897c6 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 7ce503c..0000000 --- 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 0000000..a34406f --- /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 0000000..a34406f --- /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 0000000..0035631 --- /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 0000000..c4aa31a --- /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 9bbd7d3..4e118fa 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 e0ad664..c528528 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 0000000..5be1b4a --- /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 0000000..0cdd9f8 --- /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 1da0ce1..8aff681 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 75d764e..72ee277 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 53ec207..9e74e83 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 6cd7279..6c5e066 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 0046838..eed2e70 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 0ae03fe..a32926c 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 913937d..fc65e50 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 6f3f4c2..ec56910 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 d14fdab..ee520a5 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 0000000..6849b3f --- /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 0000000..333ea94 --- /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 0000000..cb85a4b --- /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 4792120..3bfaffe 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 0000000..b457edc --- /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 0000000..8f4555e --- /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 0a198bb..1a091b0 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 685ddd2..6798b64 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 b1b59da..d6b77a4 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 a7172da..5e380ee 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 0c6f5b6..a6a124c 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 0000000..685f258 --- /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 0000000..ff87b9f --- /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 0000000..b468249 --- /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 0296b14..2a969fc 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 4371dd2..cc06e63 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 35c662d..e24a13c 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 face187..d54a1b9 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 d44c6d4..4bd61d9 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 0000000..685f258 --- /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 0000000..1c4306c --- /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 0000000..acc7da2 --- /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 a69ba4a..e04f628 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 ff5b0ca..59664fd 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 d5c884a..5d08ca2 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 870b7c6..81ba2d7 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 5491e54..038b47f 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 ae71866..58a2856 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 cf51885..047e285 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 a78116f..c540e78 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 0000000..3cbb437 --- /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 0000000..2ec7f0f --- /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 a3672d4..260251a 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 6dcaa33..b36e2a0 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 b4f6527..0000000 --- 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 f22b9c1..bf34e61 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 - - Returns all parcels of the requested geometry rendered into a simple HTML table - - Args: - request (HttpRequest): The incoming request - id (str): The geometry's id - - 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) - - 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_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) - +class GeomParcelsView(LoginRequiredMixin, View): + + def get(self, request: HttpRequest, id: str): + """ Getter for HTMX + + Returns all parcels of the requested geometry rendered into a simple HTML table + + Args: + request (HttpRequest): The incoming request + id (str): The geometry's id + + 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) + + 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_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 743ef32..5253bbf 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 943673d..fe4d0db 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 2f155e4..cbfcbda 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 7c17cb7..4eef6ff 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 ad7a3ec..c01dbc4 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 0000000..4301bfc --- /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 0000000..48ecd5d --- /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 0000000..4c9338a --- /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) +