From 345b2664223a36c42c20354d7f9c5747b565846b Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 15 Aug 2023 11:29:38 +0200 Subject: [PATCH 01/21] Class based views * refactors method based views for parcel fetching, home and logout to class based --- .gitignore | 4 +- konova/urls.py | 16 ++-- konova/views/geometry.py | 170 ++++++++++++++++++++------------------- konova/views/home.py | 103 ++++++++++++------------ konova/views/logout.py | 22 ++--- 5 files changed, 163 insertions(+), 152 deletions(-) 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/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/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) From d9cf9669f0833bf29e62648c4301249fd6bc96df Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 17 Aug 2023 10:12:05 +0200 Subject: [PATCH 02/21] Unit test analysis * adds unit test for creating report * fixes bug where new (>2018) eco accounts have not been fetched correctly from the db * adds enhancements in the frontend * improves test data setup --- analysis/settings.py | 6 +- .../templates/analysis/reports/detail.html | 2 +- .../compensation/card_compensation.html | 1 + .../eco_account/card_eco_account.html | 1 + .../intervention/card_intervention.html | 1 + ..._interventions.html => card_old_data.html} | 0 analysis/tests.py | 3 - analysis/tests/__init__.py | 7 ++ analysis/tests/unit/__init__.py | 7 ++ analysis/tests/unit/test_forms.py | 47 +++++++++ analysis/tests/unit/test_report.py | 94 ++++++++++++++++++ analysis/utils/report.py | 5 +- konova/tests/test_views.py | 42 +++++--- locale/de/LC_MESSAGES/django.mo | Bin 47373 -> 47450 bytes locale/de/LC_MESSAGES/django.po | 27 +++-- 15 files changed, 211 insertions(+), 32 deletions(-) rename analysis/templates/analysis/reports/includes/old_data/{card_old_interventions.html => card_old_data.html} (100%) delete mode 100644 analysis/tests.py create mode 100644 analysis/tests/__init__.py create mode 100644 analysis/tests/unit/__init__.py create mode 100644 analysis/tests/unit/test_forms.py create mode 100644 analysis/tests/unit/test_report.py 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..ee7518c --- /dev/null +++ b/analysis/tests/unit/test_report.py @@ -0,0 +1,94 @@ +""" +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() + + 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/konova/tests/test_views.py b/konova/tests/test_views.py index 437f114..608336f 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -65,7 +65,7 @@ class BaseTestCase(TestCase): self.compensation = self.create_dummy_compensation() self.eco_account = self.create_dummy_eco_account() self.ema = self.create_dummy_ema() - self.deduction = self.create_dummy_deduction() + self.deduction = self.create_dummy_deduction(acc=self.eco_account, interv=self.intervention) self.create_dummy_states() self.create_dummy_action() self.codes = self.create_dummy_codes() @@ -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,12 +280,14 @@ 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): diff --git a/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 93455db5c5099dcbb30abfce3b358354e4bc2346..a2539a32229cf9b9b47dfb17e8d7f3256ced972e 100644 GIT binary patch delta 12689 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 P-t9Vc-QIe{mh%4xEJZ8) delta 12662 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 diff --git a/locale/de/LC_MESSAGES/django.po b/locale/de/LC_MESSAGES/django.po index dc2c627..b917005 100644 --- a/locale/de/LC_MESSAGES/django.po +++ b/locale/de/LC_MESSAGES/django.po @@ -43,7 +43,7 @@ msgid "" msgstr "" "Project-Id-Version: PACKAGE VERSION\n" "Report-Msgid-Bugs-To: \n" -"POT-Creation-Date: 2023-07-10 10:16+0200\n" +"POT-Creation-Date: 2023-08-17 10:09+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" @@ -2291,7 +2298,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" @@ -2299,13 +2306,13 @@ msgstr "Home" msgid "Log" msgstr "Log" -#: konova/views/map_proxy.py:71 +#: konova/views/map_proxy.py:70 msgid "" "The external service is currently unavailable.
Please try again in a few " "moments..." msgstr "" -"Der externe Dienst ist zur Zeit nicht erreichbar.
Versuchen Sie es in ein paar " -"Sekunden nochmal." +"Der externe Dienst ist zur Zeit nicht erreichbar.
Versuchen Sie es in ein " +"paar Sekunden nochmal." #: konova/views/record.py:30 msgid "{} unrecorded" From 947671391112f756dd168705f0dc707381f9bb6f Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 17 Aug 2023 10:44:58 +0200 Subject: [PATCH 03/21] Unit test api * adds unit test for APIUserToken * enhances handling of token fetching for API --- api/models/token.py | 6 +-- api/tests/unit/__init__.py | 7 ++++ api/tests/unit/test_token.py | 71 ++++++++++++++++++++++++++++++++++++ api/views/views.py | 8 +++- 4 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 api/tests/unit/__init__.py create mode 100644 api/tests/unit/test_token.py 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/views/views.py b/api/views/views.py index 75d764e..f3a86bc 100644 --- a/api/views/views.py +++ b/api/views/views.py @@ -53,7 +53,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") From 564ae4d5db2d175631c6d8fcb87900b4ad48acdd Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 17 Aug 2023 12:59:50 +0200 Subject: [PATCH 04/21] Tests on analysis and compensation * enhances tests for analysis and compensation app --- analysis/tests/unit/test_report.py | 4 ++++ compensation/tests/compensation/test_workflow.py | 1 + compensation/tests/ecoaccount/test_workflow.py | 11 ++++++++--- intervention/tests/test_workflow.py | 3 ++- konova/tests/test_views.py | 2 +- 5 files changed, 16 insertions(+), 5 deletions(-) diff --git a/analysis/tests/unit/test_report.py b/analysis/tests/unit/test_report.py index ee7518c..c4aa31a 100644 --- a/analysis/tests/unit/test_report.py +++ b/analysis/tests/unit/test_report.py @@ -57,6 +57,10 @@ class TimeSpanReportTestCase(BaseTestCase): 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() 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/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/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/konova/tests/test_views.py b/konova/tests/test_views.py index 608336f..baeebdd 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -65,7 +65,7 @@ class BaseTestCase(TestCase): self.compensation = self.create_dummy_compensation() self.eco_account = self.create_dummy_eco_account() self.ema = self.create_dummy_ema() - self.deduction = self.create_dummy_deduction(acc=self.eco_account, interv=self.intervention) + self.deduction = self.create_dummy_deduction() self.create_dummy_states() self.create_dummy_action() self.codes = self.create_dummy_codes() From 643abcf84176b3ade8c71777dcdbfbcd5029cd07 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 21 Aug 2023 10:10:23 +0200 Subject: [PATCH 05/21] Unit test for compensation forms * adds compensation action forms unit tests --- .../forms/modals/compensation_action.py | 2 +- .../tests/compensation/unit/__init__.py | 7 + .../tests/compensation/unit/test_forms.py | 154 ++++++++++++++++++ 3 files changed, 162 insertions(+), 1 deletion(-) create mode 100644 compensation/tests/compensation/unit/__init__.py create mode 100644 compensation/tests/compensation/unit/test_forms.py 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/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..3250f70 --- /dev/null +++ b/compensation/tests/compensation/unit/test_forms.py @@ -0,0 +1,154 @@ +""" +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 +from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \ + EditCompensationActionModalForm, RemoveCompensationActionModalForm +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 +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 From 1d3b576b517628ad13dfd01d04e67ddf462678b0 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 21 Aug 2023 10:33:05 +0200 Subject: [PATCH 06/21] Unit test for compensation forms * adds unit tests for adding and editing deadline --- .../tests/compensation/unit/test_forms.py | 97 ++++++++++++++++++- 1 file changed, 96 insertions(+), 1 deletion(-) diff --git a/compensation/tests/compensation/unit/test_forms.py b/compensation/tests/compensation/unit/test_forms.py index 3250f70..8e3eb15 100644 --- a/compensation/tests/compensation/unit/test_forms.py +++ b/compensation/tests/compensation/unit/test_forms.py @@ -5,19 +5,25 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 21.08.23 """ +from datetime import timedelta + +from dateutil.parser import parse from django.core.exceptions import ObjectDoesNotExist from django.test import RequestFactory +from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from codelist.models import KonovaCodeList from codelist.settings import CODELIST_COMPENSATION_ACTION_ID from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \ EditCompensationActionModalForm, RemoveCompensationActionModalForm +from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm from compensation.models import UnitChoices +from konova.models import DeadlineType 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 + COMPENSATION_ACTION_REMOVED, ADDED_DEADLINE, DEADLINE_EDITED from user.models import UserAction @@ -152,3 +158,92 @@ class RemoveCompensationActionModalFormTestCase(EditCompensationActionModalFormT self.fail(msg="This action should not be fetchable anymore") except ObjectDoesNotExist: pass + + +class NewDeadlineModalFormTestCase(BaseTestCase): + def setUp(self) -> None: + super().setUp() + self.request = RequestFactory().request() + self.request.user = self.superuser + + def test_init(self): + form = NewDeadlineModalForm( + request=self.request, + instance=self.compensation + ) + self.assertEqual(form.form_title, str(_("New deadline"))) + self.assertEqual(form.form_caption, str(_("Insert data for the new deadline"))) + + def test_save(self): + deadline_type = DeadlineType.MAINTAIN + deadline_date = now().date() + timedelta(days=500) + deadline_comment = generate_random_string(50, use_letters_uc=True, use_letters_lc=True) + + data = { + "type": deadline_type, + "date": deadline_date, + "comment": deadline_comment, + } + + form = NewDeadlineModalForm(data, request=self.request, instance=self.compensation) + self.assertTrue(form.is_valid()) + + deadline = form.save() + 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_DEADLINE) + + self.assertEqual(deadline.date, deadline_date) + self.assertEqual(deadline.type, deadline_type) + self.assertEqual(deadline.comment, deadline_comment) + self.assertIn(deadline, self.compensation.deadlines.all()) + + +class EditDeadlineModalFormTestCase(NewDeadlineModalFormTestCase): + def setUp(self) -> None: + super().setUp() + self.compensation.deadlines.add(self.finished_deadline) + + def test_init(self): + form = EditDeadlineModalForm( + request=self.request, + instance=self.compensation, + deadline=self.finished_deadline + ) + self.assertEqual(form.deadline, self.finished_deadline) + self.assertEqual(form.form_title, str(_("Edit deadline"))) + self.assertEqual(form.fields["type"].initial, self.finished_deadline.type) + self.assertEqual(form.fields["date"].initial, self.finished_deadline.date) + self.assertEqual(form.fields["comment"].initial, self.finished_deadline.comment) + + def test_save(self): + edit_type = DeadlineType.MAINTAIN + edit_date = parse(self.finished_deadline.date).date() - timedelta(days=5) + edit_comment = generate_random_string(length=40, use_letters_lc=True) + + data = { + "type": edit_type, + "date": edit_date, + "comment": edit_comment, + } + form = EditDeadlineModalForm( + data, + request=self.request, + instance=self.compensation, + deadline=self.finished_deadline + ) + self.assertTrue(form.is_valid()) + + deadline = form.save() + self.assertEqual(deadline.type, edit_type) + self.assertEqual(deadline.date, edit_date) + self.assertEqual(deadline.comment, edit_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, DEADLINE_EDITED) + + self.assertIn(deadline, self.compensation.deadlines.all()) From 0f757a5de1605e1cc9f352cf68e917f730765317 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 22 Aug 2023 10:54:20 +0200 Subject: [PATCH 07/21] Unit test compensation states * adds unit test for adding/editing/removing compensation states --- .../tests/compensation/unit/test_forms.py | 167 +++++++++++++++++- 1 file changed, 165 insertions(+), 2 deletions(-) diff --git a/compensation/tests/compensation/unit/test_forms.py b/compensation/tests/compensation/unit/test_forms.py index 8e3eb15..82fb90e 100644 --- a/compensation/tests/compensation/unit/test_forms.py +++ b/compensation/tests/compensation/unit/test_forms.py @@ -14,16 +14,19 @@ from django.utils.timezone import now from django.utils.translation import gettext_lazy as _ from codelist.models import KonovaCodeList -from codelist.settings import CODELIST_COMPENSATION_ACTION_ID +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.deadline import NewDeadlineModalForm, EditDeadlineModalForm +from compensation.forms.modals.state import NewCompensationStateModalForm, EditCompensationStateModalForm, \ + RemoveCompensationStateModalForm from compensation.models import UnitChoices from konova.models import DeadlineType 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_DEADLINE, DEADLINE_EDITED + COMPENSATION_ACTION_REMOVED, ADDED_DEADLINE, DEADLINE_EDITED, ADDED_COMPENSATION_STATE, COMPENSATION_STATE_EDITED, \ + COMPENSATION_STATE_REMOVED from user.models import UserAction @@ -247,3 +250,163 @@ class EditDeadlineModalFormTestCase(NewDeadlineModalFormTestCase): self.assertEqual(last_log.comment, DEADLINE_EDITED) self.assertIn(deadline, self.compensation.deadlines.all()) + + +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) + From 21c788955197e41ccd401d28e682d981664af7b6 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 24 Aug 2023 10:59:32 +0200 Subject: [PATCH 08/21] Unit test EMA model * adds unit test for EMA model --- ema/models/ema.py | 9 ++-- ema/settings.py | 4 +- ema/tests/unit/__init__.py | 7 +++ ema/tests/unit/test_models.py | 90 +++++++++++++++++++++++++++++++++++ konova/models/object.py | 6 +-- 5 files changed, 108 insertions(+), 8 deletions(-) create mode 100644 ema/tests/unit/__init__.py create mode 100644 ema/tests/unit/test_models.py diff --git a/ema/models/ema.py b/ema/models/ema.py index a7172da..8d62758 100644 --- a/ema/models/ema.py +++ b/ema/models/ema.py @@ -122,7 +122,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 +139,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_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/konova/models/object.py b/konova/models/object.py index 5491e54..4f996f5 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -25,7 +25,7 @@ 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 ema.settings import EMA_IDENTIFIER_LENGTH, EMA_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 @@ -211,8 +211,8 @@ class BaseObject(BaseResource, DeletableObjectMixin): "template": ECO_ACCOUNT_IDENTIFIER_TEMPLATE, }, Ema: { - "length": EMA_ACCOUNT_IDENTIFIER_LENGTH, - "template": EMA_ACCOUNT_IDENTIFIER_TEMPLATE, + "length": EMA_IDENTIFIER_LENGTH, + "template": EMA_IDENTIFIER_TEMPLATE, }, } From a2b44a9c45289ed1960c7d4dea17e2f41df9588c Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 24 Aug 2023 11:47:40 +0200 Subject: [PATCH 09/21] Unit test intervention forms * adds unit test for new/edit intervention forms * improves code base for generating new identifiers --- compensation/models/compensation.py | 4 + compensation/models/eco_account.py | 4 + ema/models/ema.py | 4 + intervention/models/intervention.py | 5 +- intervention/tests/unit/__init__.py | 7 ++ intervention/tests/unit/test_forms.py | 123 ++++++++++++++++++++++++++ konova/models/object.py | 35 ++------ 7 files changed, 153 insertions(+), 29 deletions(-) create mode 100644 intervention/tests/unit/__init__.py create mode 100644 intervention/tests/unit/test_forms.py diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index 3a23816..b2f35f3 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 @@ -296,6 +297,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) diff --git a/compensation/models/eco_account.py b/compensation/models/eco_account.py index 35e4c02..b14f641 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,6 +53,9 @@ 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})" diff --git a/ema/models/ema.py b/ema/models/ema.py index 8d62758..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) diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index face187..22847f7 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})" 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..f50d6d1 --- /dev/null +++ b/intervention/tests/unit/test_forms.py @@ -0,0 +1,123 @@ +""" +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 django.urls import reverse +from django.utils.timezone import now +from django.utils.translation import gettext_lazy as _ + +from intervention.forms.intervention import NewInterventionForm, EditInterventionForm +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 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"]) diff --git a/konova/models/object.py b/konova/models/object.py index 4f996f5..1fe0ceb 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -143,6 +143,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 +196,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_IDENTIFIER_LENGTH, - "template": EMA_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 +208,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): From 5684b9b6d9e09e89864b2dcc1e7656e0c2339399 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 30 Aug 2023 10:37:16 +0200 Subject: [PATCH 10/21] Unit test compensation models * adds unit tests for compensation models * removes duplicated unit tests --- compensation/models/compensation.py | 11 +- compensation/models/eco_account.py | 7 +- .../tests/compensation/unit/test_forms.py | 98 +-------- .../tests/compensation/unit/test_models.py | 201 ++++++++++++++++++ intervention/models/intervention.py | 7 +- 5 files changed, 220 insertions(+), 104 deletions(-) create mode 100644 compensation/tests/compensation/unit/test_models.py diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index dd06175..d09599d 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -399,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: @@ -408,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 @@ -513,8 +513,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 f667eef..b1584cb 100644 --- a/compensation/models/eco_account.py +++ b/compensation/models/eco_account.py @@ -240,8 +240,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/tests/compensation/unit/test_forms.py b/compensation/tests/compensation/unit/test_forms.py index 82fb90e..333ea94 100644 --- a/compensation/tests/compensation/unit/test_forms.py +++ b/compensation/tests/compensation/unit/test_forms.py @@ -5,27 +5,21 @@ Contact: ksp-servicestelle@sgdnord.rlp.de Created on: 21.08.23 """ -from datetime import timedelta - -from dateutil.parser import parse from django.core.exceptions import ObjectDoesNotExist from django.test import RequestFactory -from django.utils.timezone import now 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.deadline import NewDeadlineModalForm, EditDeadlineModalForm from compensation.forms.modals.state import NewCompensationStateModalForm, EditCompensationStateModalForm, \ RemoveCompensationStateModalForm from compensation.models import UnitChoices -from konova.models import DeadlineType 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_DEADLINE, DEADLINE_EDITED, ADDED_COMPENSATION_STATE, COMPENSATION_STATE_EDITED, \ + COMPENSATION_ACTION_REMOVED, ADDED_COMPENSATION_STATE, COMPENSATION_STATE_EDITED, \ COMPENSATION_STATE_REMOVED from user.models import UserAction @@ -163,95 +157,6 @@ class RemoveCompensationActionModalFormTestCase(EditCompensationActionModalFormT pass -class NewDeadlineModalFormTestCase(BaseTestCase): - def setUp(self) -> None: - super().setUp() - self.request = RequestFactory().request() - self.request.user = self.superuser - - def test_init(self): - form = NewDeadlineModalForm( - request=self.request, - instance=self.compensation - ) - self.assertEqual(form.form_title, str(_("New deadline"))) - self.assertEqual(form.form_caption, str(_("Insert data for the new deadline"))) - - def test_save(self): - deadline_type = DeadlineType.MAINTAIN - deadline_date = now().date() + timedelta(days=500) - deadline_comment = generate_random_string(50, use_letters_uc=True, use_letters_lc=True) - - data = { - "type": deadline_type, - "date": deadline_date, - "comment": deadline_comment, - } - - form = NewDeadlineModalForm(data, request=self.request, instance=self.compensation) - self.assertTrue(form.is_valid()) - - deadline = form.save() - 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_DEADLINE) - - self.assertEqual(deadline.date, deadline_date) - self.assertEqual(deadline.type, deadline_type) - self.assertEqual(deadline.comment, deadline_comment) - self.assertIn(deadline, self.compensation.deadlines.all()) - - -class EditDeadlineModalFormTestCase(NewDeadlineModalFormTestCase): - def setUp(self) -> None: - super().setUp() - self.compensation.deadlines.add(self.finished_deadline) - - def test_init(self): - form = EditDeadlineModalForm( - request=self.request, - instance=self.compensation, - deadline=self.finished_deadline - ) - self.assertEqual(form.deadline, self.finished_deadline) - self.assertEqual(form.form_title, str(_("Edit deadline"))) - self.assertEqual(form.fields["type"].initial, self.finished_deadline.type) - self.assertEqual(form.fields["date"].initial, self.finished_deadline.date) - self.assertEqual(form.fields["comment"].initial, self.finished_deadline.comment) - - def test_save(self): - edit_type = DeadlineType.MAINTAIN - edit_date = parse(self.finished_deadline.date).date() - timedelta(days=5) - edit_comment = generate_random_string(length=40, use_letters_lc=True) - - data = { - "type": edit_type, - "date": edit_date, - "comment": edit_comment, - } - form = EditDeadlineModalForm( - data, - request=self.request, - instance=self.compensation, - deadline=self.finished_deadline - ) - self.assertTrue(form.is_valid()) - - deadline = form.save() - self.assertEqual(deadline.type, edit_type) - self.assertEqual(deadline.date, edit_date) - self.assertEqual(deadline.comment, edit_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, DEADLINE_EDITED) - - self.assertIn(deadline, self.compensation.deadlines.all()) - - class NewCompensationStateModalFormTestCase(BaseTestCase): def setUp(self) -> None: super().setUp() @@ -371,6 +276,7 @@ class EditCompensationStateModalFormTestCase(NewCompensationStateModalFormTestCa self.assertEqual(last_log.user, self.superuser) self.assertEqual(last_log.comment, COMPENSATION_STATE_EDITED) + class RemoveCompensationStateModalFormTestCase(EditCompensationStateModalFormTestCase): def setUp(self) -> None: super().setUp() 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/intervention/models/intervention.py b/intervention/models/intervention.py index 22847f7..b54fa8f 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -419,8 +419,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)) From 3fa76063d01f791c98b4146315dc03668a24369b Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 30 Aug 2023 16:20:06 +0200 Subject: [PATCH 11/21] Eco account unit tests * adds eco account unit tests * adds validity check to eco account form to check on existing deductions and potential conflict with reduced deductable surface * improves geojson handling on SimpleGeomForm * adds/updates translation --- compensation/forms/eco_account.py | 17 ++ compensation/models/eco_account.py | 14 -- .../tests/ecoaccount/unit/__init__.py | 7 + .../tests/ecoaccount/unit/test_models.py | 128 ++++++++++++++ compensation/views/eco_account/eco_account.py | 4 +- konova/forms/geometry_form.py | 5 +- locale/de/LC_MESSAGES/django.mo | Bin 47502 -> 47250 bytes locale/de/LC_MESSAGES/django.po | 161 ++++++------------ 8 files changed, 209 insertions(+), 127 deletions(-) create mode 100644 compensation/tests/ecoaccount/unit/__init__.py create mode 100644 compensation/tests/ecoaccount/unit/test_models.py 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/models/eco_account.py b/compensation/models/eco_account.py index b1584cb..fc65e50 100644 --- a/compensation/models/eco_account.py +++ b/compensation/models/eco_account.py @@ -59,20 +59,6 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix 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 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/eco_account/eco_account.py b/compensation/views/eco_account/eco_account.py index 685ddd2..840a39c 100644 --- a/compensation/views/eco_account/eco_account.py +++ b/compensation/views/eco_account/eco_account.py @@ -150,7 +150,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)) 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/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index 2f155e471e2cdb4654112d59a1edb25d5a55b0af..e5844ea944eb086d7bc0c948b8ee5234484d0af7 100644 GIT binary patch delta 12915 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 TuGHjF+%q}XH8zbKW1Rm7qlbw% delta 13063 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\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" @@ -484,7 +485,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 " @@ -597,16 +606,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" @@ -640,18 +652,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 @@ -706,10 +721,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" @@ -722,6 +739,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" @@ -755,21 +773,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 @@ -1424,6 +1427,7 @@ msgid "Binding on" msgstr "Datum Bestandskraft bzw. Rechtskraft" #: intervention/forms/intervention.py:216 +#: intervention/tests/unit/test_forms.py:27 #: intervention/views/intervention.py:105 msgid "New intervention" msgstr "Neuer Eingriff" @@ -1802,7 +1806,6 @@ msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?" #: konova/forms/modals/document_form.py:49 #: venv/lib/python3.7/site-packages/django/db/models/fields/files.py:231 -#: venv_py3.9/lib/python3.9/site-packages/django/db/models/fields/files.py:231 msgid "File" msgstr "Datei" @@ -2234,15 +2237,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." @@ -2250,20 +2249,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" @@ -2297,7 +2296,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" @@ -2305,7 +2304,7 @@ msgstr "Home" msgid "Log" msgstr "Log" -#: konova/views/map_proxy.py:71 +#: konova/views/map_proxy.py:70 msgid "" "The external service is currently unavailable.
Please try again in a few " "moments..." @@ -3118,26 +3117,20 @@ msgstr "Team verlassen" #: venv/lib/python3.7/site-packages/bootstrap4/components.py:17 #: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3 #: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/messages.html:4 -#: venv_py3.9/lib/python3.9/site-packages/bootstrap4/components.py:17 -#: venv_py3.9/lib/python3.9/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3 -#: venv_py3.9/lib/python3.9/site-packages/bootstrap4/templates/bootstrap4/messages.html:4 msgid "close" msgstr "Schließen" #: venv/lib/python3.7/site-packages/click/_termui_impl.py:496 -#: venv_py3.9/lib/python3.9/site-packages/click/_termui_impl.py:496 #, python-brace-format msgid "{editor}: Editing failed" msgstr "" #: venv/lib/python3.7/site-packages/click/_termui_impl.py:500 -#: venv_py3.9/lib/python3.9/site-packages/click/_termui_impl.py:500 #, python-brace-format msgid "{editor}: Editing failed: {e}" msgstr "" #: venv/lib/python3.7/site-packages/click/_unicodefun.py:20 -#: venv_py3.9/lib/python3.9/site-packages/click/_unicodefun.py:20 msgid "" "Click will abort further execution because Python was configured to use " "ASCII as encoding for the environment. Consult https://click.palletsprojects." @@ -3145,7 +3138,6 @@ msgid "" msgstr "" #: venv/lib/python3.7/site-packages/click/_unicodefun.py:56 -#: venv_py3.9/lib/python3.9/site-packages/click/_unicodefun.py:56 msgid "" "Additional information: on this system no suitable UTF-8 locales were " "discovered. This most likely requires resolving by reconfiguring the locale " @@ -3153,14 +3145,12 @@ msgid "" msgstr "" #: venv/lib/python3.7/site-packages/click/_unicodefun.py:65 -#: venv_py3.9/lib/python3.9/site-packages/click/_unicodefun.py:65 msgid "" "This system supports the C.UTF-8 locale which is recommended. You might be " "able to resolve your issue by exporting the following environment variables:" msgstr "" #: venv/lib/python3.7/site-packages/click/_unicodefun.py:75 -#: venv_py3.9/lib/python3.9/site-packages/click/_unicodefun.py:75 #, python-brace-format msgid "" "This system lists some UTF-8 supporting locales that you can pick from. The " @@ -3168,7 +3158,6 @@ msgid "" msgstr "" #: venv/lib/python3.7/site-packages/click/_unicodefun.py:93 -#: venv_py3.9/lib/python3.9/site-packages/click/_unicodefun.py:93 msgid "" "Click discovered that you exported a UTF-8 locale but the locale system " "could not pick up from it because it does not exist. The exported locale is " @@ -3176,32 +3165,25 @@ msgid "" msgstr "" #: venv/lib/python3.7/site-packages/click/core.py:1095 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:1095 msgid "Aborted!" msgstr "" #: venv/lib/python3.7/site-packages/click/core.py:1279 #: venv/lib/python3.7/site-packages/click/decorators.py:434 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:1279 -#: venv_py3.9/lib/python3.9/site-packages/click/decorators.py:434 msgid "Show this message and exit." msgstr "" #: venv/lib/python3.7/site-packages/click/core.py:1308 #: venv/lib/python3.7/site-packages/click/core.py:1334 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:1308 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:1334 #, python-brace-format msgid "(Deprecated) {text}" msgstr "" #: venv/lib/python3.7/site-packages/click/core.py:1351 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:1351 msgid "Options" msgstr "Optionen" #: venv/lib/python3.7/site-packages/click/core.py:1375 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:1375 #, python-brace-format msgid "Got unexpected extra argument ({args})" msgid_plural "Got unexpected extra arguments ({args})" @@ -3209,32 +3191,26 @@ msgstr[0] "" msgstr[1] "" #: venv/lib/python3.7/site-packages/click/core.py:1390 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:1390 msgid "DeprecationWarning: The command {name!r} is deprecated." msgstr "" #: venv/lib/python3.7/site-packages/click/core.py:1607 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:1607 msgid "Commands" msgstr "Befehle" #: venv/lib/python3.7/site-packages/click/core.py:1639 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:1639 msgid "Missing command." msgstr "Befehl fehlt" #: venv/lib/python3.7/site-packages/click/core.py:1717 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:1717 msgid "No such command {name!r}." msgstr "" #: venv/lib/python3.7/site-packages/click/core.py:2258 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:2258 msgid "Value must be an iterable." msgstr "" #: venv/lib/python3.7/site-packages/click/core.py:2278 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:2278 #, python-brace-format msgid "Takes {nargs} values but 1 was given." msgid_plural "Takes {nargs} values but {len} were given." @@ -3242,99 +3218,81 @@ msgstr[0] "" msgstr[1] "" #: venv/lib/python3.7/site-packages/click/core.py:2701 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:2701 #, python-brace-format msgid "env var: {var}" msgstr "" #: venv/lib/python3.7/site-packages/click/core.py:2724 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:2724 msgid "(dynamic)" msgstr "" #: venv/lib/python3.7/site-packages/click/core.py:2735 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:2735 #, python-brace-format msgid "default: {default}" msgstr "" #: venv/lib/python3.7/site-packages/click/core.py:2748 -#: venv_py3.9/lib/python3.9/site-packages/click/core.py:2748 msgid "required" msgstr "" #: venv/lib/python3.7/site-packages/click/decorators.py:339 -#: venv_py3.9/lib/python3.9/site-packages/click/decorators.py:339 #, python-format msgid "%(prog)s, version %(version)s" msgstr "" #: venv/lib/python3.7/site-packages/click/decorators.py:403 -#: venv_py3.9/lib/python3.9/site-packages/click/decorators.py:403 msgid "Show the version and exit." msgstr "" #: venv/lib/python3.7/site-packages/click/exceptions.py:43 #: venv/lib/python3.7/site-packages/click/exceptions.py:79 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:43 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:79 #, python-brace-format msgid "Error: {message}" msgstr "" #: venv/lib/python3.7/site-packages/click/exceptions.py:71 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:71 #, python-brace-format msgid "Try '{command} {option}' for help." msgstr "" #: venv/lib/python3.7/site-packages/click/exceptions.py:120 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:120 #, python-brace-format msgid "Invalid value: {message}" msgstr "" #: venv/lib/python3.7/site-packages/click/exceptions.py:122 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:122 #, python-brace-format msgid "Invalid value for {param_hint}: {message}" msgstr "" #: venv/lib/python3.7/site-packages/click/exceptions.py:178 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:178 msgid "Missing argument" msgstr "Argument fehlt" #: venv/lib/python3.7/site-packages/click/exceptions.py:180 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:180 msgid "Missing option" msgstr "Option fehlt" #: venv/lib/python3.7/site-packages/click/exceptions.py:182 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:182 msgid "Missing parameter" msgstr "Parameter fehlt" #: venv/lib/python3.7/site-packages/click/exceptions.py:184 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:184 #, python-brace-format msgid "Missing {param_type}" msgstr "" #: venv/lib/python3.7/site-packages/click/exceptions.py:191 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:191 #, python-brace-format msgid "Missing parameter: {param_name}" msgstr "" #: venv/lib/python3.7/site-packages/click/exceptions.py:211 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:211 #, python-brace-format msgid "No such option: {name}" msgstr "" #: venv/lib/python3.7/site-packages/click/exceptions.py:223 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:223 #, python-brace-format msgid "Did you mean {possibility}?" msgid_plural "(Possible options: {possibilities})" @@ -3342,75 +3300,61 @@ msgstr[0] "" msgstr[1] "" #: venv/lib/python3.7/site-packages/click/exceptions.py:261 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:261 msgid "unknown error" msgstr "" #: venv/lib/python3.7/site-packages/click/exceptions.py:268 -#: venv_py3.9/lib/python3.9/site-packages/click/exceptions.py:268 msgid "Could not open file {filename!r}: {message}" msgstr "" #: venv/lib/python3.7/site-packages/click/parser.py:231 -#: venv_py3.9/lib/python3.9/site-packages/click/parser.py:231 msgid "Argument {name!r} takes {nargs} values." msgstr "" #: venv/lib/python3.7/site-packages/click/parser.py:413 -#: venv_py3.9/lib/python3.9/site-packages/click/parser.py:413 msgid "Option {name!r} does not take a value." msgstr "" #: venv/lib/python3.7/site-packages/click/parser.py:474 -#: venv_py3.9/lib/python3.9/site-packages/click/parser.py:474 msgid "Option {name!r} requires an argument." msgid_plural "Option {name!r} requires {nargs} arguments." msgstr[0] "" msgstr[1] "" #: venv/lib/python3.7/site-packages/click/shell_completion.py:316 -#: venv_py3.9/lib/python3.9/site-packages/click/shell_completion.py:316 msgid "Shell completion is not supported for Bash versions older than 4.4." msgstr "" #: venv/lib/python3.7/site-packages/click/shell_completion.py:322 -#: venv_py3.9/lib/python3.9/site-packages/click/shell_completion.py:322 msgid "Couldn't detect Bash version, shell completion is not supported." msgstr "" #: venv/lib/python3.7/site-packages/click/termui.py:161 -#: venv_py3.9/lib/python3.9/site-packages/click/termui.py:161 msgid "Repeat for confirmation" msgstr "" #: venv/lib/python3.7/site-packages/click/termui.py:178 -#: venv_py3.9/lib/python3.9/site-packages/click/termui.py:178 msgid "Error: The value you entered was invalid." msgstr "" #: venv/lib/python3.7/site-packages/click/termui.py:180 -#: venv_py3.9/lib/python3.9/site-packages/click/termui.py:180 #, python-brace-format msgid "Error: {e.message}" msgstr "" #: venv/lib/python3.7/site-packages/click/termui.py:191 -#: venv_py3.9/lib/python3.9/site-packages/click/termui.py:191 msgid "Error: The two entered values do not match." msgstr "" #: venv/lib/python3.7/site-packages/click/termui.py:247 -#: venv_py3.9/lib/python3.9/site-packages/click/termui.py:247 msgid "Error: invalid input" msgstr "" #: venv/lib/python3.7/site-packages/click/termui.py:798 -#: venv_py3.9/lib/python3.9/site-packages/click/termui.py:798 msgid "Press any key to continue..." msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:258 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:258 #, python-brace-format msgid "" "Choose from:\n" @@ -3418,82 +3362,67 @@ msgid "" msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:290 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:290 msgid "{value!r} is not {choice}." msgid_plural "{value!r} is not one of {choices}." msgstr[0] "" msgstr[1] "" #: venv/lib/python3.7/site-packages/click/types.py:380 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:380 msgid "{value!r} does not match the format {format}." msgid_plural "{value!r} does not match the formats {formats}." msgstr[0] "" msgstr[1] "" #: venv/lib/python3.7/site-packages/click/types.py:402 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:402 msgid "{value!r} is not a valid {number_type}." msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:458 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:458 #, python-brace-format msgid "{value} is not in the range {range}." msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:599 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:599 msgid "{value!r} is not a valid boolean." msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:623 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:623 msgid "{value!r} is not a valid UUID." msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:801 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:801 msgid "file" msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:803 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:803 msgid "directory" msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:805 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:805 msgid "path" msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:851 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:851 msgid "{name} {filename!r} does not exist." msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:860 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:860 msgid "{name} {filename!r} is a file." msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:868 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:868 msgid "{name} {filename!r} is a directory." msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:876 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:876 msgid "{name} {filename!r} is not writable." msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:884 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:884 msgid "{name} {filename!r} is not readable." msgstr "" #: venv/lib/python3.7/site-packages/click/types.py:951 -#: venv_py3.9/lib/python3.9/site-packages/click/types.py:951 #, python-brace-format msgid "{len_type} values are required, but {len_value} was given." msgid_plural "{len_type} values are required, but {len_value} were given." @@ -3501,22 +3430,18 @@ msgstr[0] "" msgstr[1] "" #: venv/lib/python3.7/site-packages/django/contrib/messages/apps.py:7 -#: venv_py3.9/lib/python3.9/site-packages/django/contrib/messages/apps.py:7 msgid "Messages" msgstr "Nachrichten" #: venv/lib/python3.7/site-packages/django/contrib/sitemaps/apps.py:7 -#: venv_py3.9/lib/python3.9/site-packages/django/contrib/sitemaps/apps.py:7 msgid "Site Maps" msgstr "" #: venv/lib/python3.7/site-packages/django/contrib/staticfiles/apps.py:9 -#: venv_py3.9/lib/python3.9/site-packages/django/contrib/staticfiles/apps.py:9 msgid "Static Files" msgstr "" #: venv/lib/python3.7/site-packages/django/contrib/syndication/apps.py:7 -#: venv_py3.9/lib/python3.9/site-packages/django/contrib/syndication/apps.py:7 msgid "Syndication" msgstr "" @@ -4688,6 +4613,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" From 2fa8783fd957303e267fb1015b21a2395b877a2a Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 31 Aug 2023 11:31:33 +0200 Subject: [PATCH 12/21] Deduction validity checking * fixes behaviour of related deduction checks on intervention checking --- intervention/forms/modals/check.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) 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 From 963dd57cb28d90dc29867b48e10f0e34e062cbc6 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 31 Aug 2023 12:17:28 +0200 Subject: [PATCH 13/21] Unit tests intervention * adds tests for share and revocation forms --- intervention/tests/unit/test_forms.py | 210 ++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) diff --git a/intervention/tests/unit/test_forms.py b/intervention/tests/unit/test_forms.py index f50d6d1..1c4306c 100644 --- a/intervention/tests/unit/test_forms.py +++ b/intervention/tests/unit/test_forms.py @@ -6,15 +6,24 @@ 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 @@ -121,3 +130,204 @@ class EditInterventionFormTestCase(NewInterventionFormTestCase): 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) From e22c9132e19e7b36df01f3aeafeed82e8911383c Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 5 Sep 2023 11:24:29 +0200 Subject: [PATCH 14/21] Unit tests ema * adds unit tests for ema forms --- ema/forms.py | 12 +-- ema/tests/unit/test_forms.py | 141 +++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 ema/tests/unit/test_forms.py 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/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)) + From b09ad302b54751e17b023394d2cd4ba12380d3c4 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 7 Sep 2023 10:48:11 +0200 Subject: [PATCH 15/21] Unit test intervention/konova * adds unit test for intervention app * adds unit test for konova app --- intervention/tests/unit/test_models.py | 50 +++++ konova/tests/test_views.py | 15 +- konova/tests/unit/test_forms.py | 289 +++++++++++++++++++++++++ 3 files changed, 353 insertions(+), 1 deletion(-) create mode 100644 intervention/tests/unit/test_models.py create mode 100644 konova/tests/unit/test_forms.py 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/konova/tests/test_views.py b/konova/tests/test_views.py index 1ad02f1..81736c8 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 @@ -420,6 +420,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..d13ec8e --- /dev/null +++ b/konova/tests/unit/test_forms.py @@ -0,0 +1,289 @@ +""" +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 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 +from konova.models import DeadlineType +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) From 42cb138276b2a16374b68c1b18f82006998d37ec Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 7 Sep 2023 11:07:17 +0200 Subject: [PATCH 16/21] Unit test konova base forms * adds unit test for resubmission modal form --- konova/tests/unit/test_forms.py | 80 ++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 2 deletions(-) diff --git a/konova/tests/unit/test_forms.py b/konova/tests/unit/test_forms.py index d13ec8e..3cbb437 100644 --- a/konova/tests/unit/test_forms.py +++ b/konova/tests/unit/test_forms.py @@ -6,6 +6,7 @@ Created on: 07.09.23 """ import mimetypes +from datetime import timedelta from django.core.exceptions import ObjectDoesNotExist from django.test import RequestFactory @@ -19,8 +20,8 @@ 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 -from konova.models import DeadlineType + 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 @@ -287,3 +288,78 @@ class RemoveDeadlineTestCase(BaseTestCase): 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) From e443c5f8be9da567424cbf0634fa91d9a0c101e2 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 8 Sep 2023 12:47:50 +0200 Subject: [PATCH 17/21] # Unit test konova app * adds unit test for konova app models * drops unused/unnecessary code fragments * updates translation --- compensation/models/compensation.py | 7 +- .../views/compensation/compensation.py | 13 +- intervention/models/intervention.py | 10 +- intervention/views/intervention.py | 9 +- konova/forms/remove_form.py | 6 + konova/models/object.py | 31 +-- konova/tests/test_views.py | 7 +- konova/tests/unit/test_models.py | 182 ++++++++++++++++++ konova/utils/message_templates.py | 2 +- locale/de/LC_MESSAGES/django.mo | Bin 47250 -> 47228 bytes locale/de/LC_MESSAGES/django.po | 73 ++++--- 11 files changed, 255 insertions(+), 85 deletions(-) create mode 100644 konova/tests/unit/test_models.py diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index d09599d..a32926c 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -421,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 diff --git a/compensation/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 0a198bb..1b4691c 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -25,7 +25,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 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 @@ -170,15 +170,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( diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index b54fa8f..d54a1b9 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -279,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 diff --git a/intervention/views/intervention.py b/intervention/views/intervention.py index a69ba4a..681fc3b 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -22,7 +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 + CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED from konova.utils.user_checks import in_group @@ -230,12 +230,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/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/object.py b/konova/models/object.py index 1fe0ceb..51817cb 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_IDENTIFIER_LENGTH, EMA_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): @@ -298,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 @@ -353,7 +328,7 @@ class CheckableObjectMixin(models.Model): abstract = True def set_unchecked(self) -> None: - """ Perform unrecording + """ Perform unchecking Args: @@ -363,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() diff --git a/konova/tests/test_views.py b/konova/tests/test_views.py index 81736c8..6eebfa1 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -290,7 +290,7 @@ class BaseTestCase(TestCase): ]) return codes - def create_dummy_team(self): + def create_dummy_team(self, name: str = None): """ Creates a dummy team Returns: @@ -299,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) diff --git a/konova/tests/unit/test_models.py b/konova/tests/unit/test_models.py new file mode 100644 index 0000000..8f0e881 --- /dev/null +++ b/konova/tests/unit/test_models.py @@ -0,0 +1,182 @@ +""" +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 intervention.forms.modals.share import ShareModalForm +from konova.models import DeadlineType +from konova.settings import ZB_GROUP +from konova.tests.test_views import BaseTestCase +from konova.utils.user_checks import is_default_group_only +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(is_default_group_only(self.user)) + self.assertFalse(is_default_group_only(self.superuser)) + + 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)) 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/locale/de/LC_MESSAGES/django.mo b/locale/de/LC_MESSAGES/django.mo index e5844ea944eb086d7bc0c948b8ee5234484d0af7..cbfcbda46c4d13446b591eba2bfb1aa43bd5b008 100644 GIT binary patch delta 5870 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 Z%!=<3Kc=8)e!=9tCkqRzr|b&9>;F\n" "Language-Team: LANGUAGE \n" @@ -401,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" @@ -490,8 +491,8 @@ 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." +"{}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." @@ -935,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" @@ -997,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" @@ -1013,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" @@ -1187,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" @@ -1291,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 {}" @@ -1316,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" @@ -1427,7 +1430,7 @@ msgid "Binding on" msgstr "Datum Bestandskraft bzw. Rechtskraft" #: intervention/forms/intervention.py:216 -#: intervention/tests/unit/test_forms.py:27 +#: intervention/tests/unit/test_forms.py:36 #: intervention/views/intervention.py:105 msgid "New intervention" msgstr "Neuer Eingriff" @@ -1450,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." @@ -1512,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" @@ -1553,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 {}" @@ -1668,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" @@ -1790,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." @@ -1813,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" @@ -1821,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?" @@ -1855,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." @@ -1862,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" @@ -2077,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 "" From 1773463e862359ebed5fa54b270035bb978ca898 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 12 Sep 2023 09:16:10 +0200 Subject: [PATCH 18/21] # Unit tests for konova geometry * adds further unit tests for konova app geometry model --- konova/models/geometry.py | 2 +- konova/sub_settings/lanis_settings.py | 2 +- konova/tests/test_geometries.py | 66 +++++++++++++++++++++++++++ konova/tests/test_views.py | 2 +- 4 files changed, 69 insertions(+), 3 deletions(-) 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/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 6eebfa1..c540e78 100644 --- a/konova/tests/test_views.py +++ b/konova/tests/test_views.py @@ -333,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): From 21db8227f8ebb11860adba35283045d9c82eb020 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 12 Sep 2023 11:49:12 +0200 Subject: [PATCH 19/21] # Unit test user * adds unit test for team creating and editing of user app --- user/tests/unit/__init__.py | 7 ++ user/tests/unit/test_forms.py | 149 ++++++++++++++++++++++++++++++++++ 2 files changed, 156 insertions(+) create mode 100644 user/tests/unit/__init__.py create mode 100644 user/tests/unit/test_forms.py 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..9aac1d1 --- /dev/null +++ b/user/tests/unit/test_forms.py @@ -0,0 +1,149 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: ksp-servicestelle@sgdnord.rlp.de +Created on: 12.09.23 + +""" +from django.test import RequestFactory +from django.urls import reverse +from django.utils.translation import gettext_lazy as _ + +from konova.tests.test_views import BaseTestCase +from user.forms.modals.team import NewTeamModalForm, EditTeamModalForm +from user.models import Team + + +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) + From d9046eb2b9c8d3287ab47ad1603d1b540a12ce72 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 13 Sep 2023 09:49:40 +0200 Subject: [PATCH 20/21] Unit test user app * adds unit test for User model and forms * refactors functions from user_checks.py into User class and drops user_checks.py --- api/tests/v1/share/test_api_sharing.py | 3 +- api/views/views.py | 3 +- compensation/models/payment.py | 2 - .../views/compensation/compensation.py | 7 +- compensation/views/eco_account/eco_account.py | 9 +- ema/views/ema.py | 7 +- intervention/forms/modals/share.py | 3 +- intervention/views/intervention.py | 7 +- konova/models/object.py | 3 +- konova/tests/unit/test_models.py | 27 +++- konova/utils/user_checks.py | 37 ----- user/models/user.py | 23 +++ user/tests/unit/test_forms.py | 141 +++++++++++++++++- user/tests/unit/test_models.py | 61 ++++++++ 14 files changed, 263 insertions(+), 70 deletions(-) delete mode 100644 konova/utils/user_checks.py create mode 100644 user/tests/unit/test_models.py 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 f3a86bc..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 @@ -321,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/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/views/compensation/compensation.py b/compensation/views/compensation/compensation.py index 1b4691c..1a091b0 100644 --- a/compensation/views/compensation/compensation.py +++ b/compensation/views/compensation/compensation.py @@ -27,7 +27,6 @@ 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, 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 @@ -265,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 840a39c..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 @@ -244,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, @@ -277,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/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/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/views/intervention.py b/intervention/views/intervention.py index 681fc3b..e04f628 100644 --- a/intervention/views/intervention.py +++ b/intervention/views/intervention.py @@ -23,7 +23,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 DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \ CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED -from konova.utils.user_checks import in_group @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}", diff --git a/konova/models/object.py b/konova/models/object.py index 51817cb..038b47f 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -640,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/tests/unit/test_models.py b/konova/tests/unit/test_models.py index 8f0e881..2ec7f0f 100644 --- a/konova/tests/unit/test_models.py +++ b/konova/tests/unit/test_models.py @@ -6,12 +6,12 @@ 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 +from konova.models import DeadlineType, Resubmission from konova.settings import ZB_GROUP from konova.tests.test_views import BaseTestCase -from konova.utils.user_checks import is_default_group_only from user.models import UserAction @@ -171,8 +171,8 @@ class ShareableObjectMixinTestCase(BaseTestCase): self.intervention.share_with_user(self.user) self.intervention.share_with_user(self.superuser) - self.assertTrue(is_default_group_only(self.user)) - self.assertFalse(is_default_group_only(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)) @@ -180,3 +180,22 @@ class ShareableObjectMixinTestCase(BaseTestCase): 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/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/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/test_forms.py b/user/tests/unit/test_forms.py index 9aac1d1..48ecd5d 100644 --- a/user/tests/unit/test_forms.py +++ b/user/tests/unit/test_forms.py @@ -5,13 +5,16 @@ 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 -from user.models import Team +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): @@ -147,3 +150,137 @@ class EditTeamModalFormTestCase(NewTeamModalFormTestCase): 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) + From 5f45db671605d619f7bbb5a20ba3e4a515110ef4 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 13 Sep 2023 14:40:22 +0200 Subject: [PATCH 21/21] # Bugfix Payment date invalid * fixes a bug where validation check of payment 'due' has not been triggered properly --- compensation/forms/modals/payment.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) 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(