From 881da38538fb9edf73ab956f7883b4246fb771bd Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 21 Jan 2022 15:26:08 +0100 Subject: [PATCH 01/36] #31 API basic implementation * adds new app to project * adds relation between User model and new APIUserToken model * adds first implementation for GET of intervention * adds basic code layout for future extension by having new versions --- api/__init__.py | 0 api/admin.py | 16 ++++++ api/apps.py | 5 ++ api/models/__init__.py | 8 +++ api/models/token.py | 23 ++++++++ api/tests/__init__.py | 7 +++ api/tests/test_api.py | 3 + api/urls/__init__.py | 8 +++ api/urls/urls.py | 14 +++++ api/urls/v1/__init__.py | 7 +++ api/urls/v1/urls.py | 14 +++++ api/views/__init__.py | 8 +++ api/views/v1/__init__.py | 7 +++ api/views/v1/general.py | 77 ++++++++++++++++++++++++++ api/views/v1/intervention.py | 40 +++++++++++++ api/views/views.py | 55 ++++++++++++++++++ konova/sub_settings/django_settings.py | 1 + konova/urls.py | 1 + konova/utils/generators.py | 13 +++++ user/models/user.py | 7 +++ 20 files changed, 314 insertions(+) create mode 100644 api/__init__.py create mode 100644 api/admin.py create mode 100644 api/apps.py create mode 100644 api/models/__init__.py create mode 100644 api/models/token.py create mode 100644 api/tests/__init__.py create mode 100644 api/tests/test_api.py create mode 100644 api/urls/__init__.py create mode 100644 api/urls/urls.py create mode 100644 api/urls/v1/__init__.py create mode 100644 api/urls/v1/urls.py create mode 100644 api/views/__init__.py create mode 100644 api/views/v1/__init__.py create mode 100644 api/views/v1/general.py create mode 100644 api/views/v1/intervention.py create mode 100644 api/views/views.py diff --git a/api/__init__.py b/api/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/api/admin.py b/api/admin.py new file mode 100644 index 0000000..c5e1fba --- /dev/null +++ b/api/admin.py @@ -0,0 +1,16 @@ +from django.contrib import admin + +from api.models.token import APIUserToken + + +class APITokenAdmin(admin.ModelAdmin): + list_display = [ + "token", + "valid_until", + "is_active", + ] + readonly_fields = [ + "token" + ] + +admin.site.register(APIUserToken, APITokenAdmin) diff --git a/api/apps.py b/api/apps.py new file mode 100644 index 0000000..d87006d --- /dev/null +++ b/api/apps.py @@ -0,0 +1,5 @@ +from django.apps import AppConfig + + +class ApiConfig(AppConfig): + name = 'api' diff --git a/api/models/__init__.py b/api/models/__init__.py new file mode 100644 index 0000000..d43f0c4 --- /dev/null +++ b/api/models/__init__.py @@ -0,0 +1,8 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" +from .token import * \ No newline at end of file diff --git a/api/models/token.py b/api/models/token.py new file mode 100644 index 0000000..f52cf7c --- /dev/null +++ b/api/models/token.py @@ -0,0 +1,23 @@ +from django.db import models + +from konova.utils.generators import generate_token + + +class APIUserToken(models.Model): + token = models.CharField( + primary_key=True, + max_length=1000, + default=generate_token, + ) + valid_until = models.DateField( + blank=True, + null=True, + help_text="Token is only valid until this date", + ) + is_active = models.BooleanField( + default=False, + help_text="Must be activated by an admin" + ) + + def __str__(self): + return self.token diff --git a/api/tests/__init__.py b/api/tests/__init__.py new file mode 100644 index 0000000..d49ff47 --- /dev/null +++ b/api/tests/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" diff --git a/api/tests/test_api.py b/api/tests/test_api.py new file mode 100644 index 0000000..7ce503c --- /dev/null +++ b/api/tests/test_api.py @@ -0,0 +1,3 @@ +from django.test import TestCase + +# Create your tests here. diff --git a/api/urls/__init__.py b/api/urls/__init__.py new file mode 100644 index 0000000..14cc6f5 --- /dev/null +++ b/api/urls/__init__.py @@ -0,0 +1,8 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" +from .urls import * diff --git a/api/urls/urls.py b/api/urls/urls.py new file mode 100644 index 0000000..62aa73d --- /dev/null +++ b/api/urls/urls.py @@ -0,0 +1,14 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" +from django.urls import path, include + +app_name = "api" + +urlpatterns = [ + path("v1/", include("api.urls.v1.urls")), +] \ No newline at end of file diff --git a/api/urls/v1/__init__.py b/api/urls/v1/__init__.py new file mode 100644 index 0000000..c6636d5 --- /dev/null +++ b/api/urls/v1/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" \ No newline at end of file diff --git a/api/urls/v1/urls.py b/api/urls/v1/urls.py new file mode 100644 index 0000000..ff02623 --- /dev/null +++ b/api/urls/v1/urls.py @@ -0,0 +1,14 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" +from django.urls import path + +from api.views.v1.intervention import APIInterventionView + +urlpatterns = [ + path("intervention/", APIInterventionView.as_view(), name="api-intervention"), +] diff --git a/api/views/__init__.py b/api/views/__init__.py new file mode 100644 index 0000000..8d87c34 --- /dev/null +++ b/api/views/__init__.py @@ -0,0 +1,8 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" +from .v1 import * diff --git a/api/views/v1/__init__.py b/api/views/v1/__init__.py new file mode 100644 index 0000000..d49ff47 --- /dev/null +++ b/api/views/v1/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" diff --git a/api/views/v1/general.py b/api/views/v1/general.py new file mode 100644 index 0000000..10f801a --- /dev/null +++ b/api/views/v1/general.py @@ -0,0 +1,77 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" +from django.db.models import QuerySet + +from api.views.views import AbstractModelAPIView +from codelist.models import KonovaCode +from intervention.models import Responsibility, Legal + + +class AbstractModelAPIViewV1(AbstractModelAPIView): + """ Holds general serialization functions for API v1 + + """ + + def konova_code_to_json(self, konova_code: KonovaCode): + return { + "atom_id": konova_code.atom_id, + "long_name": konova_code.long_name, + "short_name": konova_code.short_name, + } + + def responsible_to_json(self, responsible: Responsibility): + return { + "registration_office": self.konova_code_to_json(responsible.registration_office), + "registration_file_number": responsible.registration_file_number, + "conservation_office": self.konova_code_to_json(responsible.conservation_office), + "conservation_file_number": responsible.conservation_file_number, + "handler": responsible.handler, + } + + def legal_to_json(self, legal: Legal): + return { + "registration_date": legal.registration_date, + "binding_date": legal.binding_date, + "process_type": self.konova_code_to_json(legal.process_type), + "laws": [self.konova_code_to_json(law) for law in legal.laws.all()], + } + + def payments_to_json(self, qs: QuerySet): + """ Serializes payments into json + + Args: + qs (QuerySet): A queryset of Payment entries + + Returns: + + """ + return [ + { + "amount": entry.amount, + "due_on": entry.due_on, + "comment": entry.comment, + } + for entry in qs + ] + + def deductions_to_json(self, qs: QuerySet): + """ Serializes eco account deductions into json + + Args: + qs (QuerySet): A queryset of EcoAccountDeduction entries + + Returns: + + """ + return [ + { + "eco_account": entry.account.pk, + "surface": entry.surface, + } + for entry in qs + ] \ No newline at end of file diff --git a/api/views/v1/intervention.py b/api/views/v1/intervention.py new file mode 100644 index 0000000..6085d0d --- /dev/null +++ b/api/views/v1/intervention.py @@ -0,0 +1,40 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" +import json + +from django.http import HttpRequest, JsonResponse + +from api.views.v1.general import AbstractModelAPIViewV1 +from intervention.models import Intervention + + +class APIInterventionView(AbstractModelAPIViewV1): + model = Intervention + fields_to_serialize = { + "identifier", + "title", + } + + def get(self, request: HttpRequest, identifier): + data = self.fetch_and_serialize("identifier", identifier) + return JsonResponse(data) + + def model_to_json(self, entry: Intervention): + entry_json = { + "identifier": entry.identifier, + "title": entry.title, + "responsible": self.responsible_to_json(entry.responsible), + "legal": self.legal_to_json(entry.legal), + "compensations": list(entry.compensations.all().values_list("pk", flat=True)), + "payments": self.payments_to_json(entry.payments.all()), + "deductions": self.deductions_to_json(entry.deductions.all()), + } + geom = entry.geometry.geom.geojson + geo_json = json.loads(geom) + geo_json["properties"] = entry_json + return geo_json \ No newline at end of file diff --git a/api/views/views.py b/api/views/views.py new file mode 100644 index 0000000..e04027b --- /dev/null +++ b/api/views/views.py @@ -0,0 +1,55 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" +from abc import abstractmethod + +from django.views import View + + +class AbstractModelAPIView(View): + """ Base class for API views + + The API must follow the GeoJSON Specification RFC 7946 + https://geojson.org/ + https://datatracker.ietf.org/doc/html/rfc7946 + + """ + model = None + + class Meta: + abstract = True + + @abstractmethod + def model_to_json(self, entry): + """ Defines the returned json values of the model + + Args: + entry (): The found entry from the database + + Returns: + + """ + raise NotImplementedError("Must be implemented in subclasses") + + def fetch_and_serialize(self, lookup_field, lookup_val): + """ Serializes the model entry according to the given lookup data + + Args: + lookup_field (): Which field used for lookup + lookup_val (): Value for lookup + + Returns: + serialized_data (dict) + """ + _filters = { + lookup_field: lookup_val + } + qs = self.model.objects.filter(**_filters) + serialized_data = {} + for entry in qs: + serialized_data[str(entry.pk)] = self.model_to_json(entry) + return serialized_data diff --git a/konova/sub_settings/django_settings.py b/konova/sub_settings/django_settings.py index bc1a751..052332f 100644 --- a/konova/sub_settings/django_settings.py +++ b/konova/sub_settings/django_settings.py @@ -70,6 +70,7 @@ INSTALLED_APPS = [ 'ema', 'codelist', 'analysis', + 'api', ] if DEBUG: INSTALLED_APPS += [ diff --git a/konova/urls.py b/konova/urls.py index f4de5ec..642b144 100644 --- a/konova/urls.py +++ b/konova/urls.py @@ -38,6 +38,7 @@ urlpatterns = [ path('news/', include("news.urls")), path('cl/', include("codelist.urls")), path('analysis/', include("analysis.urls")), + path('api/', include("api.urls")), # Generic deadline routes path('deadline//remove', remove_deadline_view, name="deadline-remove"), diff --git a/konova/utils/generators.py b/konova/utils/generators.py index e8cc9d2..78e075a 100644 --- a/konova/utils/generators.py +++ b/konova/utils/generators.py @@ -13,6 +13,19 @@ import qrcode.image.svg from io import BytesIO +def generate_token() -> str: + """ Shortcut for default generating of e.g. API token + + Returns: + token (str) + """ + return generate_random_string( + length=64, + use_numbers=True, + use_letters_lc=True + ) + + def generate_random_string(length: int, use_numbers: bool = False, use_letters_lc: bool = False, use_letters_uc: bool = False) -> str: """ Generates a random string of variable length diff --git a/user/models/user.py b/user/models/user.py index 837f0c6..c461751 100644 --- a/user/models/user.py +++ b/user/models/user.py @@ -15,6 +15,13 @@ from user.enums import UserNotificationEnum class User(AbstractUser): notifications = models.ManyToManyField("user.UserNotification", related_name="+", blank=True) + api_token = models.OneToOneField( + "api.APIUserToken", + blank=True, + null=True, + help_text="The user's API token", + on_delete=models.SET_NULL + ) def is_notification_setting_set(self, notification_enum: UserNotificationEnum): return self.notifications.filter( From 3938db189339e71300381e27fc050fcab6ddd36a Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 21 Jan 2022 16:15:16 +0100 Subject: [PATCH 02/36] #31 API basic implementation Token Authentication * adds token checking to AbstractModelAPIView * adds user accessibility filtering for intervention API v1 * extends fetch_and_serialize() method to take a dict for db filtering instead of a single field and value * organizes urlnames into supporting formats like "api:v1:intervention" --- api/models/token.py | 25 +++++++++++++++++++++++++ api/settings.py | 8 ++++++++ api/urls/urls.py | 2 +- api/urls/v1/urls.py | 5 +++-- api/views/v1/intervention.py | 13 +++++++------ api/views/views.py | 27 ++++++++++++++++++++------- 6 files changed, 64 insertions(+), 16 deletions(-) create mode 100644 api/settings.py diff --git a/api/models/token.py b/api/models/token.py index f52cf7c..81c0373 100644 --- a/api/models/token.py +++ b/api/models/token.py @@ -1,4 +1,6 @@ +from django.core.exceptions import ObjectDoesNotExist from django.db import models +from django.utils import timezone from konova.utils.generators import generate_token @@ -21,3 +23,26 @@ class APIUserToken(models.Model): def __str__(self): return self.token + + @staticmethod + def get_user_from_token(token: str): + """ Getter for the related user object + + Args: + token (str): The used token + + Returns: + user (User): Otherwise None + """ + _today = timezone.now().date() + try: + token_obj = APIUserToken.objects.get( + token=token, + ) + if not token_obj.is_active: + raise PermissionError("Token unverified") + if token_obj.valid_until is not None and token_obj.valid_until < _today: + raise PermissionError("Token validity expired") + except ObjectDoesNotExist: + raise PermissionError("Token invalid") + return token_obj.user diff --git a/api/settings.py b/api/settings.py new file mode 100644 index 0000000..d580cb8 --- /dev/null +++ b/api/settings.py @@ -0,0 +1,8 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" +KSP_TOKEN_HEADER_IDENTIFIER = "ksptoken" \ No newline at end of file diff --git a/api/urls/urls.py b/api/urls/urls.py index 62aa73d..fe0ddbb 100644 --- a/api/urls/urls.py +++ b/api/urls/urls.py @@ -10,5 +10,5 @@ from django.urls import path, include app_name = "api" urlpatterns = [ - path("v1/", include("api.urls.v1.urls")), + path("v1/", include("api.urls.v1.urls", namespace="v1")), ] \ No newline at end of file diff --git a/api/urls/v1/urls.py b/api/urls/v1/urls.py index ff02623..0d2cd6d 100644 --- a/api/urls/v1/urls.py +++ b/api/urls/v1/urls.py @@ -7,8 +7,9 @@ Created on: 21.01.22 """ from django.urls import path -from api.views.v1.intervention import APIInterventionView +from api.views.v1.intervention import APIInterventionViewV1 +app_name = "v1" urlpatterns = [ - path("intervention/", APIInterventionView.as_view(), name="api-intervention"), + path("intervention/", APIInterventionViewV1.as_view(), name="intervention"), ] diff --git a/api/views/v1/intervention.py b/api/views/v1/intervention.py index 6085d0d..613f608 100644 --- a/api/views/v1/intervention.py +++ b/api/views/v1/intervention.py @@ -13,15 +13,16 @@ from api.views.v1.general import AbstractModelAPIViewV1 from intervention.models import Intervention -class APIInterventionView(AbstractModelAPIViewV1): +class APIInterventionViewV1(AbstractModelAPIViewV1): model = Intervention - fields_to_serialize = { - "identifier", - "title", - } def get(self, request: HttpRequest, identifier): - data = self.fetch_and_serialize("identifier", identifier) + _filter = { + "identifier": identifier, + "users__in": [self.user], + "deleted__isnull": True, + } + data = self.fetch_and_serialize(_filter) return JsonResponse(data) def model_to_json(self, entry: Intervention): diff --git a/api/views/views.py b/api/views/views.py index e04027b..40f4221 100644 --- a/api/views/views.py +++ b/api/views/views.py @@ -7,8 +7,12 @@ Created on: 21.01.22 """ from abc import abstractmethod +from django.http import JsonResponse from django.views import View +from api.models import APIUserToken +from api.settings import KSP_TOKEN_HEADER_IDENTIFIER + class AbstractModelAPIView(View): """ Base class for API views @@ -19,6 +23,7 @@ class AbstractModelAPIView(View): """ model = None + user = None class Meta: abstract = True @@ -35,21 +40,29 @@ class AbstractModelAPIView(View): """ raise NotImplementedError("Must be implemented in subclasses") - def fetch_and_serialize(self, lookup_field, lookup_val): + def fetch_and_serialize(self, _filter): """ Serializes the model entry according to the given lookup data Args: - lookup_field (): Which field used for lookup - lookup_val (): Value for lookup + _filter (dict): Lookup declarations Returns: serialized_data (dict) """ - _filters = { - lookup_field: lookup_val - } - qs = self.model.objects.filter(**_filters) + qs = self.model.objects.filter(**_filter) serialized_data = {} for entry in qs: serialized_data[str(entry.pk)] = self.model_to_json(entry) return serialized_data + + def dispatch(self, request, *args, **kwargs): + try: + self.user = APIUserToken.get_user_from_token(request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)) + except PermissionError as e: + return JsonResponse( + { + "error": e.__str__() + }, + status=403 + ) + return super().dispatch(request, *args, **kwargs) From 0c35e79d04e9104d6637e7966cafcfd9c666a20f Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 21 Jan 2022 16:29:59 +0100 Subject: [PATCH 03/36] #31 API basic implementation Intervention fetch * enhances intervention fetching and serialization --- api/urls/v1/urls.py | 2 +- api/views/v1/general.py | 21 ++++++++++++--------- api/views/v1/intervention.py | 14 +++++++++++--- 3 files changed, 24 insertions(+), 13 deletions(-) diff --git a/api/urls/v1/urls.py b/api/urls/v1/urls.py index 0d2cd6d..c5c090d 100644 --- a/api/urls/v1/urls.py +++ b/api/urls/v1/urls.py @@ -11,5 +11,5 @@ from api.views.v1.intervention import APIInterventionViewV1 app_name = "v1" urlpatterns = [ - path("intervention/", APIInterventionViewV1.as_view(), name="intervention"), + path("intervention/", APIInterventionViewV1.as_view(), name="intervention"), ] diff --git a/api/views/v1/general.py b/api/views/v1/general.py index 10f801a..c07dc2c 100644 --- a/api/views/v1/general.py +++ b/api/views/v1/general.py @@ -50,14 +50,7 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): Returns: """ - return [ - { - "amount": entry.amount, - "due_on": entry.due_on, - "comment": entry.comment, - } - for entry in qs - ] + return list(qs.values("amount", "due_on", "comment")) def deductions_to_json(self, qs: QuerySet): """ Serializes eco account deductions into json @@ -70,8 +63,18 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): """ return [ { - "eco_account": entry.account.pk, + "id": entry.pk, + "eco_account": { + "id": entry.account.pk, + "identifier": entry.account.identifier, + "title": entry.account.title, + }, "surface": entry.surface, + "intervention": { + "id": entry.intervention.pk, + "identifier": entry.intervention.identifier, + "title": entry.intervention.title, + } } for entry in qs ] \ No newline at end of file diff --git a/api/views/v1/intervention.py b/api/views/v1/intervention.py index 613f608..b823bb4 100644 --- a/api/views/v1/intervention.py +++ b/api/views/v1/intervention.py @@ -7,6 +7,7 @@ Created on: 21.01.22 """ import json +from django.db.models import QuerySet from django.http import HttpRequest, JsonResponse from api.views.v1.general import AbstractModelAPIViewV1 @@ -16,22 +17,29 @@ from intervention.models import Intervention class APIInterventionViewV1(AbstractModelAPIViewV1): model = Intervention - def get(self, request: HttpRequest, identifier): + def get(self, request: HttpRequest, id): _filter = { - "identifier": identifier, + "id": id, "users__in": [self.user], "deleted__isnull": True, } data = self.fetch_and_serialize(_filter) return JsonResponse(data) + def compensations_to_json(self, qs: QuerySet): + return list( + qs.values( + "id", "identifier", "title" + ) + ) + def model_to_json(self, entry: Intervention): entry_json = { "identifier": entry.identifier, "title": entry.title, "responsible": self.responsible_to_json(entry.responsible), "legal": self.legal_to_json(entry.legal), - "compensations": list(entry.compensations.all().values_list("pk", flat=True)), + "compensations": self.compensations_to_json(entry.compensations.all()), "payments": self.payments_to_json(entry.payments.all()), "deductions": self.deductions_to_json(entry.deductions.all()), } From 897520f906f4a4e2924e1e6f07aeed49c57142ee Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 21 Jan 2022 17:32:31 +0100 Subject: [PATCH 04/36] #31 API basic implementation Compensation * adds compensation fetching for API v1 * refactors filter into predefined lookup dict of super class (needs to be customized on subclasses) --- api/urls/v1/urls.py | 2 ++ api/views/v1/compensation.py | 53 ++++++++++++++++++++++++++++++++++++ api/views/v1/general.py | 29 +++++++++++++++++++- api/views/v1/intervention.py | 14 ++++++---- api/views/views.py | 14 ++++++++-- 5 files changed, 102 insertions(+), 10 deletions(-) create mode 100644 api/views/v1/compensation.py diff --git a/api/urls/v1/urls.py b/api/urls/v1/urls.py index c5c090d..b11418f 100644 --- a/api/urls/v1/urls.py +++ b/api/urls/v1/urls.py @@ -7,9 +7,11 @@ Created on: 21.01.22 """ from django.urls import path +from api.views.v1.compensation import APICompensationViewV1 from api.views.v1.intervention import APIInterventionViewV1 app_name = "v1" urlpatterns = [ path("intervention/", APIInterventionViewV1.as_view(), name="intervention"), + path("compensation/", APICompensationViewV1.as_view(), name="compensation"), ] diff --git a/api/views/v1/compensation.py b/api/views/v1/compensation.py new file mode 100644 index 0000000..417e299 --- /dev/null +++ b/api/views/v1/compensation.py @@ -0,0 +1,53 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" +import json + +from django.http import HttpRequest, JsonResponse + +from api.views.v1.general import AbstractModelAPIViewV1 +from compensation.models import Compensation + + +class APICompensationViewV1(AbstractModelAPIViewV1): + model = Compensation + + def get(self, request: HttpRequest, id): + self.lookup["id"] = id + del self.lookup["users__in"] + self.lookup["intervention__users__in"] = [self.user] + + data = self.fetch_and_serialize() + return JsonResponse(data) + + def intervention_to_json(self, entry): + return { + "id": entry.pk, + "identifier": entry.identifier, + "title": entry.title, + } + + def model_to_json(self, entry): + modified_on = entry.modified or entry.created + modified_on = modified_on.timestamp + entry_json = { + "identifier": entry.identifier, + "title": entry.title, + "is_cef": entry.is_cef, + "is_coherence_keeping": entry.is_coherence_keeping, + "intervention": self.intervention_to_json(entry.intervention), + "before_states": self.compensation_state_to_json(entry.before_states.all()), + "after_states": self.compensation_state_to_json(entry.after_states.all()), + "actions": self.compensation_actions_to_json(entry.actions.all()), + "deadlines": self.deadlines_to_json(entry.deadlines.all()), + "modified_on": modified_on, + "created_on": entry.created.timestamp, + } + geom = entry.geometry.geom.geojson + geo_json = json.loads(geom) + geo_json["properties"] = entry_json + return geo_json \ No newline at end of file diff --git a/api/views/v1/general.py b/api/views/v1/general.py index c07dc2c..643475f 100644 --- a/api/views/v1/general.py +++ b/api/views/v1/general.py @@ -77,4 +77,31 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): } } for entry in qs - ] \ No newline at end of file + ] + + def compensation_state_to_json(self, qs: QuerySet): + return [ + { + "biotope": self.konova_code_to_json(entry.biotope_type), + "surface": entry.surface, + } + for entry in qs + ] + + def compensation_actions_to_json(self, qs: QuerySet): + return [ + { + "action": self.konova_code_to_json(entry.action_type), + "amount": entry.amount, + "unit": entry.unit, + "comment": entry.comment, + } + for entry in qs + ] + + def deadlines_to_json(self, qs: QuerySet): + return list(qs.values( + "type", + "date", + "comment", + )) \ No newline at end of file diff --git a/api/views/v1/intervention.py b/api/views/v1/intervention.py index b823bb4..59c0683 100644 --- a/api/views/v1/intervention.py +++ b/api/views/v1/intervention.py @@ -18,12 +18,10 @@ class APIInterventionViewV1(AbstractModelAPIViewV1): model = Intervention def get(self, request: HttpRequest, id): - _filter = { - "id": id, - "users__in": [self.user], - "deleted__isnull": True, - } - data = self.fetch_and_serialize(_filter) + self.lookup["id"] = id + self.lookup["users__in"] = [self.user] + + data = self.fetch_and_serialize() return JsonResponse(data) def compensations_to_json(self, qs: QuerySet): @@ -34,6 +32,8 @@ class APIInterventionViewV1(AbstractModelAPIViewV1): ) def model_to_json(self, entry: Intervention): + modified_on = entry.modified or entry.created + modified_on = modified_on.timestamp entry_json = { "identifier": entry.identifier, "title": entry.title, @@ -42,6 +42,8 @@ class APIInterventionViewV1(AbstractModelAPIViewV1): "compensations": self.compensations_to_json(entry.compensations.all()), "payments": self.payments_to_json(entry.payments.all()), "deductions": self.deductions_to_json(entry.deductions.all()), + "modified_on": modified_on, + "created_on": entry.created.timestamp, } geom = entry.geometry.geom.geojson geo_json = json.loads(geom) diff --git a/api/views/views.py b/api/views/views.py index 40f4221..f68a68a 100644 --- a/api/views/views.py +++ b/api/views/views.py @@ -24,10 +24,19 @@ class AbstractModelAPIView(View): """ model = None user = None + lookup = None class Meta: abstract = True + def __init__(self, *args, **kwargs): + self.lookup = { + "id": None, # must be set in subclasses + "deleted__isnull": True, + "users__in": [], # must be set in subclasses + } + super().__init__(*args, **kwargs) + @abstractmethod def model_to_json(self, entry): """ Defines the returned json values of the model @@ -40,16 +49,15 @@ class AbstractModelAPIView(View): """ raise NotImplementedError("Must be implemented in subclasses") - def fetch_and_serialize(self, _filter): + def fetch_and_serialize(self): """ Serializes the model entry according to the given lookup data Args: - _filter (dict): Lookup declarations Returns: serialized_data (dict) """ - qs = self.model.objects.filter(**_filter) + qs = self.model.objects.filter(**self.lookup) serialized_data = {} for entry in qs: serialized_data[str(entry.pk)] = self.model_to_json(entry) From 4f6964b04a59784ddea35c35f1d71ec9f4851df0 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 21 Jan 2022 17:44:58 +0100 Subject: [PATCH 05/36] #31 API basic implementation EcoAccount fetch * outsources json creation of modified_on an created_on to superclass * adds API support for fetching ecoaccount data --- api/urls/v1/urls.py | 2 ++ api/views/v1/compensation.py | 6 ++-- api/views/v1/ecoaccount.py | 58 ++++++++++++++++++++++++++++++++++++ api/views/v1/general.py | 10 ++++++- api/views/v1/intervention.py | 6 ++-- 5 files changed, 73 insertions(+), 9 deletions(-) create mode 100644 api/views/v1/ecoaccount.py diff --git a/api/urls/v1/urls.py b/api/urls/v1/urls.py index b11418f..75720e8 100644 --- a/api/urls/v1/urls.py +++ b/api/urls/v1/urls.py @@ -8,10 +8,12 @@ Created on: 21.01.22 from django.urls import path from api.views.v1.compensation import APICompensationViewV1 +from api.views.v1.ecoaccount import APIEcoAccountViewV1 from api.views.v1.intervention import APIInterventionViewV1 app_name = "v1" urlpatterns = [ path("intervention/", APIInterventionViewV1.as_view(), name="intervention"), path("compensation/", APICompensationViewV1.as_view(), name="compensation"), + path("ecoaccount/", APIEcoAccountViewV1.as_view(), name="ecoaccount"), ] diff --git a/api/views/v1/compensation.py b/api/views/v1/compensation.py index 417e299..19b6246 100644 --- a/api/views/v1/compensation.py +++ b/api/views/v1/compensation.py @@ -32,8 +32,6 @@ class APICompensationViewV1(AbstractModelAPIViewV1): } def model_to_json(self, entry): - modified_on = entry.modified or entry.created - modified_on = modified_on.timestamp entry_json = { "identifier": entry.identifier, "title": entry.title, @@ -44,8 +42,8 @@ class APICompensationViewV1(AbstractModelAPIViewV1): "after_states": self.compensation_state_to_json(entry.after_states.all()), "actions": self.compensation_actions_to_json(entry.actions.all()), "deadlines": self.deadlines_to_json(entry.deadlines.all()), - "modified_on": modified_on, - "created_on": entry.created.timestamp, + "modified_on": self.modified_on_to_json(entry), + "created_on": self.created_on_to_json(entry), } geom = entry.geometry.geom.geojson geo_json = json.loads(geom) diff --git a/api/views/v1/ecoaccount.py b/api/views/v1/ecoaccount.py new file mode 100644 index 0000000..caa1f24 --- /dev/null +++ b/api/views/v1/ecoaccount.py @@ -0,0 +1,58 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" +import json + +from django.http import JsonResponse, HttpRequest + +from api.views.v1.general import AbstractModelAPIViewV1 +from compensation.models import EcoAccount +from intervention.models import Legal, Responsibility + + +class APIEcoAccountViewV1(AbstractModelAPIViewV1): + model = EcoAccount + + def get(self, request: HttpRequest, id): + self.lookup["id"] = id + self.lookup["users__in"] = [self.user] + + data = self.fetch_and_serialize() + return JsonResponse(data) + + def model_to_json(self, entry): + entry_json = { + "identifier": entry.identifier, + "title": entry.title, + "deductable_surface": entry.deductable_surface, + "deductable_surface_available": entry.deductable_surface - entry.get_deductions_surface(), + "responsible": self.responsible_to_json(entry.responsible), + "legal": self.legal_to_json(entry.legal), + "before_states": self.compensation_state_to_json(entry.before_states.all()), + "after_states": self.compensation_state_to_json(entry.after_states.all()), + "actions": self.compensation_actions_to_json(entry.actions.all()), + "deadlines": self.deadlines_to_json(entry.deadlines.all()), + "deductions": self.deductions_to_json(entry.deductions.all()), + "modified_on": self.modified_on_to_json(entry), + "created_on": self.created_on_to_json(entry), + } + geom = entry.geometry.geom.geojson + geo_json = json.loads(geom) + geo_json["properties"] = entry_json + return geo_json + + def legal_to_json(self, legal: Legal): + return { + "agreement_date": legal.registration_date, + } + + def responsible_to_json(self, responsible: Responsibility): + return { + "conservation_office": self.konova_code_to_json(responsible.conservation_office), + "conservation_file_number": responsible.conservation_file_number, + "handler": responsible.handler, + } \ No newline at end of file diff --git a/api/views/v1/general.py b/api/views/v1/general.py index 643475f..b1343fe 100644 --- a/api/views/v1/general.py +++ b/api/views/v1/general.py @@ -104,4 +104,12 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): "type", "date", "comment", - )) \ No newline at end of file + )) + + def created_on_to_json(self, entry): + return entry.created.timestamp + + def modified_on_to_json(self, entry): + modified_on = entry.modified or entry.created + modified_on = modified_on.timestamp + return modified_on \ No newline at end of file diff --git a/api/views/v1/intervention.py b/api/views/v1/intervention.py index 59c0683..e8c329d 100644 --- a/api/views/v1/intervention.py +++ b/api/views/v1/intervention.py @@ -32,8 +32,6 @@ class APIInterventionViewV1(AbstractModelAPIViewV1): ) def model_to_json(self, entry: Intervention): - modified_on = entry.modified or entry.created - modified_on = modified_on.timestamp entry_json = { "identifier": entry.identifier, "title": entry.title, @@ -42,8 +40,8 @@ class APIInterventionViewV1(AbstractModelAPIViewV1): "compensations": self.compensations_to_json(entry.compensations.all()), "payments": self.payments_to_json(entry.payments.all()), "deductions": self.deductions_to_json(entry.deductions.all()), - "modified_on": modified_on, - "created_on": entry.created.timestamp, + "modified_on": self.modified_on_to_json(entry), + "created_on": self.created_on_to_json(entry), } geom = entry.geometry.geom.geojson geo_json = json.loads(geom) From 870cc96a1a33ba401e967a0aa7614f20de26b430 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 21 Jan 2022 17:49:07 +0100 Subject: [PATCH 06/36] #31 API basic implementation Ema fetch * adds API support for fetching EMA --- api/urls/v1/urls.py | 2 ++ api/views/v1/ema.py | 41 +++++++++++++++++++++++++++++++++++++++++ 2 files changed, 43 insertions(+) create mode 100644 api/views/v1/ema.py diff --git a/api/urls/v1/urls.py b/api/urls/v1/urls.py index 75720e8..3350c6b 100644 --- a/api/urls/v1/urls.py +++ b/api/urls/v1/urls.py @@ -9,6 +9,7 @@ from django.urls import path from api.views.v1.compensation import APICompensationViewV1 from api.views.v1.ecoaccount import APIEcoAccountViewV1 +from api.views.v1.ema import APIEmaViewV1 from api.views.v1.intervention import APIInterventionViewV1 app_name = "v1" @@ -16,4 +17,5 @@ urlpatterns = [ path("intervention/", APIInterventionViewV1.as_view(), name="intervention"), path("compensation/", APICompensationViewV1.as_view(), name="compensation"), path("ecoaccount/", APIEcoAccountViewV1.as_view(), name="ecoaccount"), + path("ema/", APIEmaViewV1.as_view(), name="ema"), ] diff --git a/api/views/v1/ema.py b/api/views/v1/ema.py new file mode 100644 index 0000000..ba07e50 --- /dev/null +++ b/api/views/v1/ema.py @@ -0,0 +1,41 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 21.01.22 + +""" +import json + +from django.http import JsonResponse, HttpRequest + +from api.views.v1.ecoaccount import APIEcoAccountViewV1 +from ema.models import Ema + + +class APIEmaViewV1(APIEcoAccountViewV1): + model = Ema + + def get(self, request: HttpRequest, id): + self.lookup["id"] = id + self.lookup["users__in"] = [self.user] + + data = self.fetch_and_serialize() + return JsonResponse(data) + + def model_to_json(self, entry): + entry_json = { + "identifier": entry.identifier, + "title": entry.title, + "responsible": self.responsible_to_json(entry.responsible), + "before_states": self.compensation_state_to_json(entry.before_states.all()), + "after_states": self.compensation_state_to_json(entry.after_states.all()), + "actions": self.compensation_actions_to_json(entry.actions.all()), + "deadlines": self.deadlines_to_json(entry.deadlines.all()), + "modified_on": self.modified_on_to_json(entry), + "created_on": self.created_on_to_json(entry), + } + geom = entry.geometry.geom.geojson + geo_json = json.loads(geom) + geo_json["properties"] = entry_json + return geo_json \ No newline at end of file From 8d400b4ffe9cf2d74a5139ccd7cbd49879ce4e96 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 21 Jan 2022 18:34:01 +0100 Subject: [PATCH 07/36] #31 API basic implementation Cleanup * cleans code * reworks many code fragments into smaller methods and split into super class --- api/views/v1/compensation.py | 37 +++----- api/views/v1/ecoaccount.py | 43 +++------ api/views/v1/ema.py | 43 +++------ api/views/v1/intervention.py | 34 ++----- api/views/v1/{general.py => views.py} | 127 +++++++++++++++++++++++++- api/views/views.py | 50 +++++++--- 6 files changed, 206 insertions(+), 128 deletions(-) rename api/views/v1/{general.py => views.py} (53%) diff --git a/api/views/v1/compensation.py b/api/views/v1/compensation.py index 19b6246..1ef49b9 100644 --- a/api/views/v1/compensation.py +++ b/api/views/v1/compensation.py @@ -5,25 +5,19 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 21.01.22 """ -import json -from django.http import HttpRequest, JsonResponse - -from api.views.v1.general import AbstractModelAPIViewV1 +from api.views.v1.views import AbstractModelAPIViewV1 from compensation.models import Compensation class APICompensationViewV1(AbstractModelAPIViewV1): model = Compensation - def get(self, request: HttpRequest, id): + def prepare_lookup(self, id): self.lookup["id"] = id del self.lookup["users__in"] self.lookup["intervention__users__in"] = [self.user] - data = self.fetch_and_serialize() - return JsonResponse(data) - def intervention_to_json(self, entry): return { "id": entry.pk, @@ -31,21 +25,12 @@ class APICompensationViewV1(AbstractModelAPIViewV1): "title": entry.title, } - def model_to_json(self, entry): - entry_json = { - "identifier": entry.identifier, - "title": entry.title, - "is_cef": entry.is_cef, - "is_coherence_keeping": entry.is_coherence_keeping, - "intervention": self.intervention_to_json(entry.intervention), - "before_states": self.compensation_state_to_json(entry.before_states.all()), - "after_states": self.compensation_state_to_json(entry.after_states.all()), - "actions": self.compensation_actions_to_json(entry.actions.all()), - "deadlines": self.deadlines_to_json(entry.deadlines.all()), - "modified_on": self.modified_on_to_json(entry), - "created_on": self.created_on_to_json(entry), - } - geom = entry.geometry.geom.geojson - geo_json = json.loads(geom) - geo_json["properties"] = entry_json - return geo_json \ No newline at end of file + def extend_properties_data(self, entry): + self.properties_data["is_cef"] = entry.is_cef + self.properties_data["is_coherence_keeping"] = entry.is_coherence_keeping + self.properties_data["intervention"] = self.intervention_to_json(entry.intervention) + self.properties_data["before_states"] = self.compensation_state_to_json(entry.before_states.all()) + self.properties_data["after_states"] = self.compensation_state_to_json(entry.after_states.all()) + self.properties_data["actions"] = self.compensation_actions_to_json(entry.actions.all()) + self.properties_data["deadlines"] = self.deadlines_to_json(entry.deadlines.all()) + diff --git a/api/views/v1/ecoaccount.py b/api/views/v1/ecoaccount.py index caa1f24..7339a0a 100644 --- a/api/views/v1/ecoaccount.py +++ b/api/views/v1/ecoaccount.py @@ -5,11 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 21.01.22 """ -import json - -from django.http import JsonResponse, HttpRequest - -from api.views.v1.general import AbstractModelAPIViewV1 +from api.views.v1.views import AbstractModelAPIViewV1 from compensation.models import EcoAccount from intervention.models import Legal, Responsibility @@ -17,33 +13,16 @@ from intervention.models import Legal, Responsibility class APIEcoAccountViewV1(AbstractModelAPIViewV1): model = EcoAccount - def get(self, request: HttpRequest, id): - self.lookup["id"] = id - self.lookup["users__in"] = [self.user] - - data = self.fetch_and_serialize() - return JsonResponse(data) - - def model_to_json(self, entry): - entry_json = { - "identifier": entry.identifier, - "title": entry.title, - "deductable_surface": entry.deductable_surface, - "deductable_surface_available": entry.deductable_surface - entry.get_deductions_surface(), - "responsible": self.responsible_to_json(entry.responsible), - "legal": self.legal_to_json(entry.legal), - "before_states": self.compensation_state_to_json(entry.before_states.all()), - "after_states": self.compensation_state_to_json(entry.after_states.all()), - "actions": self.compensation_actions_to_json(entry.actions.all()), - "deadlines": self.deadlines_to_json(entry.deadlines.all()), - "deductions": self.deductions_to_json(entry.deductions.all()), - "modified_on": self.modified_on_to_json(entry), - "created_on": self.created_on_to_json(entry), - } - geom = entry.geometry.geom.geojson - geo_json = json.loads(geom) - geo_json["properties"] = entry_json - return geo_json + def extend_properties_data(self, entry): + self.properties_data["deductable_surface"] = entry.deductable_surface + self.properties_data["deductable_surface_available"] = entry.deductable_surface - entry.get_deductions_surface() + self.properties_data["responsible"] = self.responsible_to_json(entry.responsible) + self.properties_data["legal"] = self.legal_to_json(entry.legal) + self.properties_data["before_states"] = self.compensation_state_to_json(entry.before_states.all()) + self.properties_data["after_states"] = self.compensation_state_to_json(entry.after_states.all()) + self.properties_data["actions"] = self.compensation_actions_to_json(entry.actions.all()) + self.properties_data["deadlines"] = self.deadlines_to_json(entry.deadlines.all()) + self.properties_data["deductions"] = self.deductions_to_json(entry.deductions.all()) def legal_to_json(self, legal: Legal): return { diff --git a/api/views/v1/ema.py b/api/views/v1/ema.py index ba07e50..991d382 100644 --- a/api/views/v1/ema.py +++ b/api/views/v1/ema.py @@ -5,37 +5,24 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 21.01.22 """ -import json - -from django.http import JsonResponse, HttpRequest - -from api.views.v1.ecoaccount import APIEcoAccountViewV1 +from api.views.v1.views import AbstractModelAPIViewV1 from ema.models import Ema +from intervention.models import Responsibility -class APIEmaViewV1(APIEcoAccountViewV1): +class APIEmaViewV1(AbstractModelAPIViewV1): model = Ema - def get(self, request: HttpRequest, id): - self.lookup["id"] = id - self.lookup["users__in"] = [self.user] - - data = self.fetch_and_serialize() - return JsonResponse(data) - - def model_to_json(self, entry): - entry_json = { - "identifier": entry.identifier, - "title": entry.title, - "responsible": self.responsible_to_json(entry.responsible), - "before_states": self.compensation_state_to_json(entry.before_states.all()), - "after_states": self.compensation_state_to_json(entry.after_states.all()), - "actions": self.compensation_actions_to_json(entry.actions.all()), - "deadlines": self.deadlines_to_json(entry.deadlines.all()), - "modified_on": self.modified_on_to_json(entry), - "created_on": self.created_on_to_json(entry), + def responsible_to_json(self, responsible: Responsibility): + return { + "conservation_office": self.konova_code_to_json(responsible.conservation_office), + "conservation_file_number": responsible.conservation_file_number, + "handler": responsible.handler, } - geom = entry.geometry.geom.geojson - geo_json = json.loads(geom) - geo_json["properties"] = entry_json - return geo_json \ No newline at end of file + + def extend_properties_data(self, entry): + self.properties_data["responsible"] = self.responsible_to_json(entry.responsible) + self.properties_data["before_states"] = self.compensation_state_to_json(entry.before_states.all()) + self.properties_data["after_states"] = self.compensation_state_to_json(entry.after_states.all()) + self.properties_data["actions"] = self.compensation_actions_to_json(entry.actions.all()) + self.properties_data["deadlines"] = self.deadlines_to_json(entry.deadlines.all()) diff --git a/api/views/v1/intervention.py b/api/views/v1/intervention.py index e8c329d..d097793 100644 --- a/api/views/v1/intervention.py +++ b/api/views/v1/intervention.py @@ -5,25 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 21.01.22 """ -import json - from django.db.models import QuerySet -from django.http import HttpRequest, JsonResponse -from api.views.v1.general import AbstractModelAPIViewV1 +from api.views.v1.views import AbstractModelAPIViewV1 from intervention.models import Intervention class APIInterventionViewV1(AbstractModelAPIViewV1): model = Intervention - def get(self, request: HttpRequest, id): - self.lookup["id"] = id - self.lookup["users__in"] = [self.user] - - data = self.fetch_and_serialize() - return JsonResponse(data) - def compensations_to_json(self, qs: QuerySet): return list( qs.values( @@ -31,19 +21,9 @@ class APIInterventionViewV1(AbstractModelAPIViewV1): ) ) - def model_to_json(self, entry: Intervention): - entry_json = { - "identifier": entry.identifier, - "title": entry.title, - "responsible": self.responsible_to_json(entry.responsible), - "legal": self.legal_to_json(entry.legal), - "compensations": self.compensations_to_json(entry.compensations.all()), - "payments": self.payments_to_json(entry.payments.all()), - "deductions": self.deductions_to_json(entry.deductions.all()), - "modified_on": self.modified_on_to_json(entry), - "created_on": self.created_on_to_json(entry), - } - geom = entry.geometry.geom.geojson - geo_json = json.loads(geom) - geo_json["properties"] = entry_json - return geo_json \ No newline at end of file + def extend_properties_data(self, entry): + self.properties_data["responsible"] = self.responsible_to_json(entry.responsible) + self.properties_data["legal"] = self.legal_to_json(entry.legal) + self.properties_data["compensations"] = self.compensations_to_json(entry.compensations.all()) + self.properties_data["payments"] = self.payments_to_json(entry.payments.all()) + self.properties_data["deductions"] = self.deductions_to_json(entry.deductions.all()) diff --git a/api/views/v1/general.py b/api/views/v1/views.py similarity index 53% rename from api/views/v1/general.py rename to api/views/v1/views.py index b1343fe..7aaeb18 100644 --- a/api/views/v1/general.py +++ b/api/views/v1/views.py @@ -5,7 +5,10 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 21.01.22 """ +import json + from django.db.models import QuerySet +from django.http import JsonResponse, HttpRequest from api.views.views import AbstractModelAPIView from codelist.models import KonovaCode @@ -17,7 +20,69 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): """ + def get(self, request: HttpRequest, id): + """ Handles the GET request + + Performs the fetching and serialization of the data + + Args: + request (HttpRequest): The incoming request + id (str): The entries id + + Returns: + + """ + self.prepare_lookup(id) + + try: + data = self.fetch_and_serialize() + except Exception as e: + return self.return_error_response(e, 500) + return JsonResponse(data) + + def model_to_geo_json(self, entry): + """ Adds the basic data, which all elements hold + + Args: + entry (): The data entry + + Returns: + + """ + geom = entry.geometry.geom.geojson + geo_json = json.loads(geom) + self.properties_data = { + "id": entry.id, + "identifier": entry.identifier, + "title": entry.title, + "created_on": self.created_on_to_json(entry), + "modified_on": self.modified_on_to_json(entry), + } + self.extend_properties_data(entry) + geo_json["properties"] = self.properties_data + return geo_json + + def prepare_lookup(self, id): + """ Customizes lookup values for db filtering + + Args: + id (str): The entries id + + Returns: + + """ + self.lookup["id"] = id + self.lookup["users__in"] = [self.user] + def konova_code_to_json(self, konova_code: KonovaCode): + """ Serializes KonovaCode model into json + + Args: + konova_code (KonovaCode): The KonovaCode entry + + Returns: + serialized_json (dict) + """ return { "atom_id": konova_code.atom_id, "long_name": konova_code.long_name, @@ -25,6 +90,14 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): } def responsible_to_json(self, responsible: Responsibility): + """ Serializes Responsibility model into json + + Args: + responsible (Responsibility): The Responsibility entry + + Returns: + serialized_json (dict) + """ return { "registration_office": self.konova_code_to_json(responsible.registration_office), "registration_file_number": responsible.registration_file_number, @@ -34,6 +107,14 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): } def legal_to_json(self, legal: Legal): + """ Serializes Legal model into json + + Args: + legal (Legal): The Legal entry + + Returns: + serialized_json (dict) + """ return { "registration_date": legal.registration_date, "binding_date": legal.binding_date, @@ -48,7 +129,7 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): qs (QuerySet): A queryset of Payment entries Returns: - + serialized_json (list) """ return list(qs.values("amount", "due_on", "comment")) @@ -59,7 +140,7 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): qs (QuerySet): A queryset of EcoAccountDeduction entries Returns: - + serialized_json (list) """ return [ { @@ -80,6 +161,14 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): ] def compensation_state_to_json(self, qs: QuerySet): + """ Serializes compensation states into json + + Args: + qs (QuerySet): A queryset of CompensationState entries + + Returns: + serialized_json (list) + """ return [ { "biotope": self.konova_code_to_json(entry.biotope_type), @@ -89,6 +178,14 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): ] def compensation_actions_to_json(self, qs: QuerySet): + """ Serializes CompensationActions into json + + Args: + qs (QuerySet): A queryset of CompensationAction entries + + Returns: + serialized_json (list) + """ return [ { "action": self.konova_code_to_json(entry.action_type), @@ -100,6 +197,14 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): ] def deadlines_to_json(self, qs: QuerySet): + """ Serializes deadlines into json + + Args: + qs (QuerySet): A queryset of Deadline entries + + Returns: + serialized_json (list) + """ return list(qs.values( "type", "date", @@ -107,9 +212,25 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): )) def created_on_to_json(self, entry): + """ Serializes the created_on into json + + Args: + entry (BaseObject): The entry + + Returns: + created_on (timestamp) + """ return entry.created.timestamp def modified_on_to_json(self, entry): + """ Serializes the modified_on into json + + Args: + entry (BaseObject): The entry + + Returns: + modified_on (timestamp) + """ modified_on = entry.modified or entry.created modified_on = modified_on.timestamp - return modified_on \ No newline at end of file + return modified_on diff --git a/api/views/views.py b/api/views/views.py index f68a68a..21b60a8 100644 --- a/api/views/views.py +++ b/api/views/views.py @@ -25,6 +25,7 @@ class AbstractModelAPIView(View): model = None user = None lookup = None + properties_data = None class Meta: abstract = True @@ -38,8 +39,20 @@ class AbstractModelAPIView(View): super().__init__(*args, **kwargs) @abstractmethod - def model_to_json(self, entry): - """ Defines the returned json values of the model + def model_to_geo_json(self, entry): + """ Defines the model as geo json + + Args: + entry (): The found entry from the database + + Returns: + + """ + raise NotImplementedError("Must be implemented in subclasses") + + @abstractmethod + def extend_properties_data(self, entry): + """ Defines the 'properties' part of geo json Args: entry (): The found entry from the database @@ -57,20 +70,33 @@ class AbstractModelAPIView(View): Returns: serialized_data (dict) """ - qs = self.model.objects.filter(**self.lookup) - serialized_data = {} - for entry in qs: - serialized_data[str(entry.pk)] = self.model_to_json(entry) + entry = self.model.objects.get(**self.lookup) + serialized_data = self.model_to_geo_json(entry) return serialized_data def dispatch(self, request, *args, **kwargs): try: self.user = APIUserToken.get_user_from_token(request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)) except PermissionError as e: - return JsonResponse( - { - "error": e.__str__() - }, - status=403 - ) + return self.return_error_response(e, 403) return super().dispatch(request, *args, **kwargs) + + def return_error_response(self, error, status_code=500): + """ Returns an error as JsonReponse + + Args: + error (): The error/exception + status_code (): The desired status code + + Returns: + + """ + content = [error.__str__()] + if hasattr(error, "messages"): + content = error.messages + return JsonResponse( + { + "errors": content + }, + status=status_code + ) From 45ac5b68b93a468d055a3b1e1dfb3116089943df Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 10:31:48 +0100 Subject: [PATCH 08/36] #31 API basic implementation Refactor * reorganizes code into proper api/utils/serializer subclasses to keep serialization logic away from regular view logic --- api/urls/v1/urls.py | 13 +- api/utils/__init__.py | 7 + api/utils/serializer.py | 76 +++++++++ api/utils/v1/__init__.py | 7 + api/{views => utils}/v1/compensation.py | 12 +- api/{views => utils}/v1/ecoaccount.py | 6 +- api/{views => utils}/v1/ema.py | 6 +- api/{views => utils}/v1/intervention.py | 9 +- api/utils/v1/serializer.py | 200 +++++++++++++++++++++++ api/views/v1/views.py | 209 ++---------------------- api/views/views.py | 43 +---- 11 files changed, 328 insertions(+), 260 deletions(-) create mode 100644 api/utils/__init__.py create mode 100644 api/utils/serializer.py create mode 100644 api/utils/v1/__init__.py rename api/{views => utils}/v1/compensation.py (82%) rename api/{views => utils}/v1/ecoaccount.py (91%) rename api/{views => utils}/v1/ema.py (88%) rename api/{views => utils}/v1/intervention.py (83%) create mode 100644 api/utils/v1/serializer.py diff --git a/api/urls/v1/urls.py b/api/urls/v1/urls.py index 3350c6b..4c92510 100644 --- a/api/urls/v1/urls.py +++ b/api/urls/v1/urls.py @@ -7,15 +7,12 @@ Created on: 21.01.22 """ from django.urls import path -from api.views.v1.compensation import APICompensationViewV1 -from api.views.v1.ecoaccount import APIEcoAccountViewV1 -from api.views.v1.ema import APIEmaViewV1 -from api.views.v1.intervention import APIInterventionViewV1 +from api.views.v1.views import EmaAPIViewV1, EcoAccountAPIViewV1, CompensationAPIViewV1, InterventionAPIViewV1 app_name = "v1" urlpatterns = [ - path("intervention/", APIInterventionViewV1.as_view(), name="intervention"), - path("compensation/", APICompensationViewV1.as_view(), name="compensation"), - path("ecoaccount/", APIEcoAccountViewV1.as_view(), name="ecoaccount"), - path("ema/", APIEmaViewV1.as_view(), name="ema"), + path("intervention/", InterventionAPIViewV1.as_view(), name="intervention"), + path("compensation/", CompensationAPIViewV1.as_view(), name="compensation"), + path("ecoaccount/", EcoAccountAPIViewV1.as_view(), name="ecoaccount"), + path("ema/", EmaAPIViewV1.as_view(), name="ema"), ] diff --git a/api/utils/__init__.py b/api/utils/__init__.py new file mode 100644 index 0000000..71a67bb --- /dev/null +++ b/api/utils/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 24.01.22 + +""" diff --git a/api/utils/serializer.py b/api/utils/serializer.py new file mode 100644 index 0000000..eb7dc60 --- /dev/null +++ b/api/utils/serializer.py @@ -0,0 +1,76 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 24.01.22 + +""" +from abc import abstractmethod + + +class AbstractModelAPISerializer: + model = None + user = None + lookup = None + properties_data = None + + class Meta: + abstract = True + + def __init__(self, *args, **kwargs): + self.lookup = { + "id": None, # must be set + "deleted__isnull": True, + "users__in": [], # must be set + } + super().__init__(*args, **kwargs) + + @abstractmethod + def model_to_geo_json(self, entry): + """ Defines the model as geo json + + Args: + entry (): The found entry from the database + + Returns: + + """ + raise NotImplementedError("Must be implemented in subclasses") + + @abstractmethod + def extend_properties_data(self, entry): + """ Defines the 'properties' part of geo json + + Args: + entry (): The found entry from the database + + Returns: + + """ + raise NotImplementedError("Must be implemented in subclasses") + + @abstractmethod + def prepare_lookup(self, _id, user): + """ Updates lookup dict for db fetching + + Args: + _id (str): The object's id + user (User): The user requesting for + + Returns: + + """ + self.lookup["id"] = _id + self.lookup["users__in"] = [user] + + def fetch_and_serialize(self): + """ Serializes the model entry according to the given lookup data + + Args: + + Returns: + serialized_data (dict) + """ + entry = self.model.objects.get(**self.lookup) + serialized_data = self.model_to_geo_json(entry) + return serialized_data diff --git a/api/utils/v1/__init__.py b/api/utils/v1/__init__.py new file mode 100644 index 0000000..71a67bb --- /dev/null +++ b/api/utils/v1/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 24.01.22 + +""" diff --git a/api/views/v1/compensation.py b/api/utils/v1/compensation.py similarity index 82% rename from api/views/v1/compensation.py rename to api/utils/v1/compensation.py index 1ef49b9..557ae13 100644 --- a/api/views/v1/compensation.py +++ b/api/utils/v1/compensation.py @@ -2,21 +2,20 @@ Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 21.01.22 +Created on: 24.01.22 """ - -from api.views.v1.views import AbstractModelAPIViewV1 +from api.utils.v1.serializer import AbstractModelAPISerializerV1 from compensation.models import Compensation -class APICompensationViewV1(AbstractModelAPIViewV1): +class CompensationAPISerializerV1(AbstractModelAPISerializerV1): model = Compensation - def prepare_lookup(self, id): + def prepare_lookup(self, id, user): self.lookup["id"] = id del self.lookup["users__in"] - self.lookup["intervention__users__in"] = [self.user] + self.lookup["intervention__users__in"] = [user] def intervention_to_json(self, entry): return { @@ -33,4 +32,3 @@ class APICompensationViewV1(AbstractModelAPIViewV1): self.properties_data["after_states"] = self.compensation_state_to_json(entry.after_states.all()) self.properties_data["actions"] = self.compensation_actions_to_json(entry.actions.all()) self.properties_data["deadlines"] = self.deadlines_to_json(entry.deadlines.all()) - diff --git a/api/views/v1/ecoaccount.py b/api/utils/v1/ecoaccount.py similarity index 91% rename from api/views/v1/ecoaccount.py rename to api/utils/v1/ecoaccount.py index 7339a0a..0aab2bb 100644 --- a/api/views/v1/ecoaccount.py +++ b/api/utils/v1/ecoaccount.py @@ -2,15 +2,15 @@ Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 21.01.22 +Created on: 24.01.22 """ -from api.views.v1.views import AbstractModelAPIViewV1 +from api.utils.v1.serializer import AbstractModelAPISerializerV1 from compensation.models import EcoAccount from intervention.models import Legal, Responsibility -class APIEcoAccountViewV1(AbstractModelAPIViewV1): +class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1): model = EcoAccount def extend_properties_data(self, entry): diff --git a/api/views/v1/ema.py b/api/utils/v1/ema.py similarity index 88% rename from api/views/v1/ema.py rename to api/utils/v1/ema.py index 991d382..66e6b40 100644 --- a/api/views/v1/ema.py +++ b/api/utils/v1/ema.py @@ -2,15 +2,15 @@ Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 21.01.22 +Created on: 24.01.22 """ -from api.views.v1.views import AbstractModelAPIViewV1 +from api.utils.v1.serializer import AbstractModelAPISerializerV1 from ema.models import Ema from intervention.models import Responsibility -class APIEmaViewV1(AbstractModelAPIViewV1): +class EmaAPISerializerV1(AbstractModelAPISerializerV1): model = Ema def responsible_to_json(self, responsible: Responsibility): diff --git a/api/views/v1/intervention.py b/api/utils/v1/intervention.py similarity index 83% rename from api/views/v1/intervention.py rename to api/utils/v1/intervention.py index d097793..76f08b1 100644 --- a/api/views/v1/intervention.py +++ b/api/utils/v1/intervention.py @@ -2,16 +2,17 @@ Author: Michel Peltriaux Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 21.01.22 +Created on: 24.01.22 """ + from django.db.models import QuerySet -from api.views.v1.views import AbstractModelAPIViewV1 +from api.utils.v1.serializer import AbstractModelAPISerializerV1 from intervention.models import Intervention -class APIInterventionViewV1(AbstractModelAPIViewV1): +class InterventionAPISerializerV1(AbstractModelAPISerializerV1): model = Intervention def compensations_to_json(self, qs: QuerySet): @@ -26,4 +27,4 @@ class APIInterventionViewV1(AbstractModelAPIViewV1): self.properties_data["legal"] = self.legal_to_json(entry.legal) self.properties_data["compensations"] = self.compensations_to_json(entry.compensations.all()) self.properties_data["payments"] = self.payments_to_json(entry.payments.all()) - self.properties_data["deductions"] = self.deductions_to_json(entry.deductions.all()) + self.properties_data["deductions"] = self.deductions_to_json(entry.deductions.all()) \ No newline at end of file diff --git a/api/utils/v1/serializer.py b/api/utils/v1/serializer.py new file mode 100644 index 0000000..0bdef78 --- /dev/null +++ b/api/utils/v1/serializer.py @@ -0,0 +1,200 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 24.01.22 + +""" + +import json + +from django.db.models import QuerySet + +from api.utils.serializer import AbstractModelAPISerializer +from codelist.models import KonovaCode +from intervention.models import Responsibility, Legal + + +class AbstractModelAPISerializerV1(AbstractModelAPISerializer): + def model_to_geo_json(self, entry): + """ Adds the basic data, which all elements hold + + Args: + entry (): The data entry + + Returns: + + """ + geom = entry.geometry.geom.geojson + geo_json = json.loads(geom) + self.properties_data = { + "id": entry.id, + "identifier": entry.identifier, + "title": entry.title, + "created_on": self.created_on_to_json(entry), + "modified_on": self.modified_on_to_json(entry), + } + self.extend_properties_data(entry) + geo_json["properties"] = self.properties_data + return geo_json + + def konova_code_to_json(self, konova_code: KonovaCode): + """ Serializes KonovaCode model into json + + Args: + konova_code (KonovaCode): The KonovaCode entry + + Returns: + serialized_json (dict) + """ + return { + "atom_id": konova_code.atom_id, + "long_name": konova_code.long_name, + "short_name": konova_code.short_name, + } + + def responsible_to_json(self, responsible: Responsibility): + """ Serializes Responsibility model into json + + Args: + responsible (Responsibility): The Responsibility entry + + Returns: + serialized_json (dict) + """ + return { + "registration_office": self.konova_code_to_json(responsible.registration_office), + "registration_file_number": responsible.registration_file_number, + "conservation_office": self.konova_code_to_json(responsible.conservation_office), + "conservation_file_number": responsible.conservation_file_number, + "handler": responsible.handler, + } + + def legal_to_json(self, legal: Legal): + """ Serializes Legal model into json + + Args: + legal (Legal): The Legal entry + + Returns: + serialized_json (dict) + """ + return { + "registration_date": legal.registration_date, + "binding_date": legal.binding_date, + "process_type": self.konova_code_to_json(legal.process_type), + "laws": [self.konova_code_to_json(law) for law in legal.laws.all()], + } + + def payments_to_json(self, qs: QuerySet): + """ Serializes payments into json + + Args: + qs (QuerySet): A queryset of Payment entries + + Returns: + serialized_json (list) + """ + return list(qs.values("amount", "due_on", "comment")) + + def deductions_to_json(self, qs: QuerySet): + """ Serializes eco account deductions into json + + Args: + qs (QuerySet): A queryset of EcoAccountDeduction entries + + Returns: + serialized_json (list) + """ + return [ + { + "id": entry.pk, + "eco_account": { + "id": entry.account.pk, + "identifier": entry.account.identifier, + "title": entry.account.title, + }, + "surface": entry.surface, + "intervention": { + "id": entry.intervention.pk, + "identifier": entry.intervention.identifier, + "title": entry.intervention.title, + } + } + for entry in qs + ] + + def compensation_state_to_json(self, qs: QuerySet): + """ Serializes compensation states into json + + Args: + qs (QuerySet): A queryset of CompensationState entries + + Returns: + serialized_json (list) + """ + return [ + { + "biotope": self.konova_code_to_json(entry.biotope_type), + "surface": entry.surface, + } + for entry in qs + ] + + def compensation_actions_to_json(self, qs: QuerySet): + """ Serializes CompensationActions into json + + Args: + qs (QuerySet): A queryset of CompensationAction entries + + Returns: + serialized_json (list) + """ + return [ + { + "action": self.konova_code_to_json(entry.action_type), + "amount": entry.amount, + "unit": entry.unit, + "comment": entry.comment, + } + for entry in qs + ] + + def deadlines_to_json(self, qs: QuerySet): + """ Serializes deadlines into json + + Args: + qs (QuerySet): A queryset of Deadline entries + + Returns: + serialized_json (list) + """ + return list(qs.values( + "type", + "date", + "comment", + )) + + def created_on_to_json(self, entry): + """ Serializes the created_on into json + + Args: + entry (BaseObject): The entry + + Returns: + created_on (timestamp) + """ + return entry.created.timestamp + + def modified_on_to_json(self, entry): + """ Serializes the modified_on into json + + Args: + entry (BaseObject): The entry + + Returns: + modified_on (timestamp) + """ + modified_on = entry.modified or entry.created + modified_on = modified_on.timestamp + return modified_on diff --git a/api/views/v1/views.py b/api/views/v1/views.py index 7aaeb18..688d4e0 100644 --- a/api/views/v1/views.py +++ b/api/views/v1/views.py @@ -5,14 +5,13 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 21.01.22 """ -import json - -from django.db.models import QuerySet from django.http import JsonResponse, HttpRequest +from api.utils.v1.compensation import CompensationAPISerializerV1 +from api.utils.v1.ecoaccount import EcoAccountAPISerializerV1 +from api.utils.v1.ema import EmaAPISerializerV1 +from api.utils.v1.intervention import InterventionAPISerializerV1 from api.views.views import AbstractModelAPIView -from codelist.models import KonovaCode -from intervention.models import Responsibility, Legal class AbstractModelAPIViewV1(AbstractModelAPIView): @@ -32,205 +31,25 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): Returns: """ - self.prepare_lookup(id) - try: - data = self.fetch_and_serialize() + self.serializer.prepare_lookup(id, self.user) + data = self.serializer.fetch_and_serialize() except Exception as e: return self.return_error_response(e, 500) return JsonResponse(data) - def model_to_geo_json(self, entry): - """ Adds the basic data, which all elements hold - - Args: - entry (): The data entry - - Returns: - - """ - geom = entry.geometry.geom.geojson - geo_json = json.loads(geom) - self.properties_data = { - "id": entry.id, - "identifier": entry.identifier, - "title": entry.title, - "created_on": self.created_on_to_json(entry), - "modified_on": self.modified_on_to_json(entry), - } - self.extend_properties_data(entry) - geo_json["properties"] = self.properties_data - return geo_json - - def prepare_lookup(self, id): - """ Customizes lookup values for db filtering - - Args: - id (str): The entries id - - Returns: - - """ - self.lookup["id"] = id - self.lookup["users__in"] = [self.user] - - def konova_code_to_json(self, konova_code: KonovaCode): - """ Serializes KonovaCode model into json - - Args: - konova_code (KonovaCode): The KonovaCode entry - - Returns: - serialized_json (dict) - """ - return { - "atom_id": konova_code.atom_id, - "long_name": konova_code.long_name, - "short_name": konova_code.short_name, - } - - def responsible_to_json(self, responsible: Responsibility): - """ Serializes Responsibility model into json - - Args: - responsible (Responsibility): The Responsibility entry - - Returns: - serialized_json (dict) - """ - return { - "registration_office": self.konova_code_to_json(responsible.registration_office), - "registration_file_number": responsible.registration_file_number, - "conservation_office": self.konova_code_to_json(responsible.conservation_office), - "conservation_file_number": responsible.conservation_file_number, - "handler": responsible.handler, - } - - def legal_to_json(self, legal: Legal): - """ Serializes Legal model into json - - Args: - legal (Legal): The Legal entry - - Returns: - serialized_json (dict) - """ - return { - "registration_date": legal.registration_date, - "binding_date": legal.binding_date, - "process_type": self.konova_code_to_json(legal.process_type), - "laws": [self.konova_code_to_json(law) for law in legal.laws.all()], - } - - def payments_to_json(self, qs: QuerySet): - """ Serializes payments into json - Args: - qs (QuerySet): A queryset of Payment entries +class InterventionAPIViewV1(AbstractModelAPIViewV1): + serializer = InterventionAPISerializerV1 - Returns: - serialized_json (list) - """ - return list(qs.values("amount", "due_on", "comment")) - def deductions_to_json(self, qs: QuerySet): - """ Serializes eco account deductions into json +class CompensationAPIViewV1(AbstractModelAPIViewV1): + serializer = CompensationAPISerializerV1 - Args: - qs (QuerySet): A queryset of EcoAccountDeduction entries - Returns: - serialized_json (list) - """ - return [ - { - "id": entry.pk, - "eco_account": { - "id": entry.account.pk, - "identifier": entry.account.identifier, - "title": entry.account.title, - }, - "surface": entry.surface, - "intervention": { - "id": entry.intervention.pk, - "identifier": entry.intervention.identifier, - "title": entry.intervention.title, - } - } - for entry in qs - ] - - def compensation_state_to_json(self, qs: QuerySet): - """ Serializes compensation states into json - - Args: - qs (QuerySet): A queryset of CompensationState entries +class EcoAccountAPIViewV1(AbstractModelAPIViewV1): + serializer = EcoAccountAPISerializerV1 - Returns: - serialized_json (list) - """ - return [ - { - "biotope": self.konova_code_to_json(entry.biotope_type), - "surface": entry.surface, - } - for entry in qs - ] - - def compensation_actions_to_json(self, qs: QuerySet): - """ Serializes CompensationActions into json - - Args: - qs (QuerySet): A queryset of CompensationAction entries - - Returns: - serialized_json (list) - """ - return [ - { - "action": self.konova_code_to_json(entry.action_type), - "amount": entry.amount, - "unit": entry.unit, - "comment": entry.comment, - } - for entry in qs - ] - - def deadlines_to_json(self, qs: QuerySet): - """ Serializes deadlines into json - Args: - qs (QuerySet): A queryset of Deadline entries - - Returns: - serialized_json (list) - """ - return list(qs.values( - "type", - "date", - "comment", - )) - - def created_on_to_json(self, entry): - """ Serializes the created_on into json - - Args: - entry (BaseObject): The entry - - Returns: - created_on (timestamp) - """ - return entry.created.timestamp - - def modified_on_to_json(self, entry): - """ Serializes the modified_on into json - - Args: - entry (BaseObject): The entry - - Returns: - modified_on (timestamp) - """ - modified_on = entry.modified or entry.created - modified_on = modified_on.timestamp - return modified_on +class EmaAPIViewV1(AbstractModelAPIViewV1): + serializer = EmaAPISerializerV1 diff --git a/api/views/views.py b/api/views/views.py index 21b60a8..4316fb9 100644 --- a/api/views/views.py +++ b/api/views/views.py @@ -5,7 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 21.01.22 """ -from abc import abstractmethod from django.http import JsonResponse from django.views import View @@ -22,10 +21,8 @@ class AbstractModelAPIView(View): https://datatracker.ietf.org/doc/html/rfc7946 """ - model = None + serializer = None user = None - lookup = None - properties_data = None class Meta: abstract = True @@ -37,45 +34,11 @@ class AbstractModelAPIView(View): "users__in": [], # must be set in subclasses } super().__init__(*args, **kwargs) - - @abstractmethod - def model_to_geo_json(self, entry): - """ Defines the model as geo json - - Args: - entry (): The found entry from the database - - Returns: - - """ - raise NotImplementedError("Must be implemented in subclasses") - - @abstractmethod - def extend_properties_data(self, entry): - """ Defines the 'properties' part of geo json - - Args: - entry (): The found entry from the database - - Returns: - - """ - raise NotImplementedError("Must be implemented in subclasses") - - def fetch_and_serialize(self): - """ Serializes the model entry according to the given lookup data - - Args: - - Returns: - serialized_data (dict) - """ - entry = self.model.objects.get(**self.lookup) - serialized_data = self.model_to_geo_json(entry) - return serialized_data + self.serializer = self.serializer() def dispatch(self, request, *args, **kwargs): try: + # Fetch the proper user from the given request header token self.user = APIUserToken.get_user_from_token(request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)) except PermissionError as e: return self.return_error_response(e, 403) From d0f3fb9f61806a81cd08374bd3657a68dcef0a28 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 12:17:17 +0100 Subject: [PATCH 09/36] #31 API POST Intervention * adds support for proper POST of intervention * makes / optional (required for Post) --- api/urls/v1/urls.py | 4 + api/utils/{v1 => serializer}/__init__.py | 0 api/utils/{ => serializer}/serializer.py | 32 ++++++- api/utils/serializer/v1/__init__.py | 7 ++ api/utils/{ => serializer}/v1/compensation.py | 4 +- api/utils/{ => serializer}/v1/ecoaccount.py | 2 +- api/utils/{ => serializer}/v1/ema.py | 2 +- api/utils/serializer/v1/intervention.py | 88 +++++++++++++++++++ api/utils/{ => serializer}/v1/serializer.py | 18 +++- api/utils/v1/intervention.py | 30 ------- api/views/v1/views.py | 23 +++-- api/views/views.py | 2 + 12 files changed, 171 insertions(+), 41 deletions(-) rename api/utils/{v1 => serializer}/__init__.py (100%) rename api/utils/{ => serializer}/serializer.py (67%) create mode 100644 api/utils/serializer/v1/__init__.py rename api/utils/{ => serializer}/v1/compensation.py (92%) rename api/utils/{ => serializer}/v1/ecoaccount.py (95%) rename api/utils/{ => serializer}/v1/ema.py (93%) create mode 100644 api/utils/serializer/v1/intervention.py rename api/utils/{ => serializer}/v1/serializer.py (91%) delete mode 100644 api/utils/v1/intervention.py diff --git a/api/urls/v1/urls.py b/api/urls/v1/urls.py index 4c92510..37cd52f 100644 --- a/api/urls/v1/urls.py +++ b/api/urls/v1/urls.py @@ -12,7 +12,11 @@ from api.views.v1.views import EmaAPIViewV1, EcoAccountAPIViewV1, CompensationAP app_name = "v1" urlpatterns = [ path("intervention/", InterventionAPIViewV1.as_view(), name="intervention"), + path("intervention/", InterventionAPIViewV1.as_view(), name="intervention"), path("compensation/", CompensationAPIViewV1.as_view(), name="compensation"), + path("compensation/", CompensationAPIViewV1.as_view(), name="compensation"), path("ecoaccount/", EcoAccountAPIViewV1.as_view(), name="ecoaccount"), + path("ecoaccount/", EcoAccountAPIViewV1.as_view(), name="ecoaccount"), path("ema/", EmaAPIViewV1.as_view(), name="ema"), + path("ema/", EmaAPIViewV1.as_view(), name="ema"), ] diff --git a/api/utils/v1/__init__.py b/api/utils/serializer/__init__.py similarity index 100% rename from api/utils/v1/__init__.py rename to api/utils/serializer/__init__.py diff --git a/api/utils/serializer.py b/api/utils/serializer/serializer.py similarity index 67% rename from api/utils/serializer.py rename to api/utils/serializer/serializer.py index eb7dc60..a2b2b4b 100644 --- a/api/utils/serializer.py +++ b/api/utils/serializer/serializer.py @@ -5,12 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 24.01.22 """ +import json from abc import abstractmethod +from django.contrib.gis import geos +from django.contrib.gis.geos import GEOSGeometry + class AbstractModelAPISerializer: model = None - user = None lookup = None properties_data = None @@ -74,3 +77,30 @@ class AbstractModelAPISerializer: entry = self.model.objects.get(**self.lookup) serialized_data = self.model_to_geo_json(entry) return serialized_data + + @abstractmethod + def create_model_from_json(self, json_model, user): + """ Creates a new instance from given json data + + Args: + json_model (dict): JSON data + user (User): The performing user + + Returns: + + """ + raise NotImplementedError("Must be implemented in subclasses") + + def create_geometry_from_json(self, geojson) -> GEOSGeometry: + """ Creates a GEOSGeometry object based on the given geojson + + Args: + geojson (str|dict): The geojson as str or dict + + Returns: + geometry (GEOSGeometry) + """ + if isinstance(geojson, dict): + geojson = json.dumps(geojson) + geometry = geos.fromstr(geojson) + return geometry \ No newline at end of file diff --git a/api/utils/serializer/v1/__init__.py b/api/utils/serializer/v1/__init__.py new file mode 100644 index 0000000..71a67bb --- /dev/null +++ b/api/utils/serializer/v1/__init__.py @@ -0,0 +1,7 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 24.01.22 + +""" diff --git a/api/utils/v1/compensation.py b/api/utils/serializer/v1/compensation.py similarity index 92% rename from api/utils/v1/compensation.py rename to api/utils/serializer/v1/compensation.py index 557ae13..44d61c7 100644 --- a/api/utils/v1/compensation.py +++ b/api/utils/serializer/v1/compensation.py @@ -5,7 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 24.01.22 """ -from api.utils.v1.serializer import AbstractModelAPISerializerV1 +from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1 from compensation.models import Compensation @@ -31,4 +31,4 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1): self.properties_data["before_states"] = self.compensation_state_to_json(entry.before_states.all()) self.properties_data["after_states"] = self.compensation_state_to_json(entry.after_states.all()) self.properties_data["actions"] = self.compensation_actions_to_json(entry.actions.all()) - self.properties_data["deadlines"] = self.deadlines_to_json(entry.deadlines.all()) + self.properties_data["deadlines"] = self.deadlines_to_json(entry.deadlines.all()) \ No newline at end of file diff --git a/api/utils/v1/ecoaccount.py b/api/utils/serializer/v1/ecoaccount.py similarity index 95% rename from api/utils/v1/ecoaccount.py rename to api/utils/serializer/v1/ecoaccount.py index 0aab2bb..2491eb1 100644 --- a/api/utils/v1/ecoaccount.py +++ b/api/utils/serializer/v1/ecoaccount.py @@ -5,7 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 24.01.22 """ -from api.utils.v1.serializer import AbstractModelAPISerializerV1 +from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1 from compensation.models import EcoAccount from intervention.models import Legal, Responsibility diff --git a/api/utils/v1/ema.py b/api/utils/serializer/v1/ema.py similarity index 93% rename from api/utils/v1/ema.py rename to api/utils/serializer/v1/ema.py index 66e6b40..e8f2228 100644 --- a/api/utils/v1/ema.py +++ b/api/utils/serializer/v1/ema.py @@ -5,7 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 24.01.22 """ -from api.utils.v1.serializer import AbstractModelAPISerializerV1 +from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1 from ema.models import Ema from intervention.models import Responsibility diff --git a/api/utils/serializer/v1/intervention.py b/api/utils/serializer/v1/intervention.py new file mode 100644 index 0000000..b3ab207 --- /dev/null +++ b/api/utils/serializer/v1/intervention.py @@ -0,0 +1,88 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 24.01.22 + +""" +from django.db import transaction +from django.db.models import QuerySet + +from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1 +from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID, \ + CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID +from intervention.models import Intervention, Responsibility, Legal +from konova.models import Geometry +from konova.tasks import celery_update_parcels +from user.models import UserActionLogEntry + + +class InterventionAPISerializerV1(AbstractModelAPISerializerV1): + model = Intervention + + def compensations_to_json(self, qs: QuerySet): + return list( + qs.values( + "id", "identifier", "title" + ) + ) + + def extend_properties_data(self, entry): + self.properties_data["responsible"] = self.responsible_to_json(entry.responsible) + self.properties_data["legal"] = self.legal_to_json(entry.legal) + self.properties_data["compensations"] = self.compensations_to_json(entry.compensations.all()) + self.properties_data["payments"] = self.payments_to_json(entry.payments.all()) + self.properties_data["deductions"] = self.deductions_to_json(entry.deductions.all()) + + def create_model_from_json(self, json_model, user): + with transaction.atomic(): + # Create geometry + json_geom = self.create_geometry_from_json(json_model) + geometry = Geometry() + geometry.geom = json_geom + + # Create linked objects + obj = Intervention() + resp = Responsibility() + legal = Legal() + created = UserActionLogEntry.get_created_action(user, comment="API Import") + obj.legal = legal + obj.created = created + obj.geometry = geometry + obj.responsible = resp + + # Fill in data to objects + properties = json_model["properties"] + obj.identifier = obj.generate_new_identifier() + obj.title = properties["title"] + obj.responsible.registration_office = self.konova_code_from_json( + properties["responsible"]["registration_office"], + CODELIST_REGISTRATION_OFFICE_ID + ) + obj.responsible.registration_file_number = properties["responsible"]["registration_file_number"] + obj.responsible.conservation_office = self.konova_code_from_json( + properties["responsible"]["conservation_office"], + CODELIST_CONSERVATION_OFFICE_ID, + ) + obj.responsible.conservation_file_number = properties["responsible"]["conservation_file_number"] + obj.responsible.handler = properties["responsible"]["handler"] + + obj.legal.registration_date = properties["legal"]["registration_date"] + obj.legal.binding_date = properties["legal"]["binding_date"] + obj.legal.process_type = self.konova_code_from_json( + properties["legal"]["process_type"], + CODELIST_PROCESS_TYPE_ID, + ) + laws = [self.konova_code_from_json(law, CODELIST_LAW_ID) for law in properties["legal"]["laws"]] + obj.legal.laws.set(laws) + + obj.responsible.save() + obj.geometry.save() + obj.legal.save() + obj.save() + + obj.users.add(user) + + celery_update_parcels.delay(geometry.id) + + return obj.id diff --git a/api/utils/v1/serializer.py b/api/utils/serializer/v1/serializer.py similarity index 91% rename from api/utils/v1/serializer.py rename to api/utils/serializer/v1/serializer.py index 0bdef78..984038d 100644 --- a/api/utils/v1/serializer.py +++ b/api/utils/serializer/v1/serializer.py @@ -10,7 +10,7 @@ import json from django.db.models import QuerySet -from api.utils.serializer import AbstractModelAPISerializer +from api.utils.serializer.serializer import AbstractModelAPISerializer from codelist.models import KonovaCode from intervention.models import Responsibility, Legal @@ -53,6 +53,22 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer): "short_name": konova_code.short_name, } + def konova_code_from_json(self, json_str, code_list_identifier): + """ Returns a konova code instance + + Args: + json_str (str): The value for the code (atom id) + code_list_identifier (str): From which konova code list this code is supposed to be from + + Returns: + + """ + code = KonovaCode.objects.get( + atom_id=json_str, + code_lists__in=[code_list_identifier] + ) + return code + def responsible_to_json(self, responsible: Responsibility): """ Serializes Responsibility model into json diff --git a/api/utils/v1/intervention.py b/api/utils/v1/intervention.py deleted file mode 100644 index 76f08b1..0000000 --- a/api/utils/v1/intervention.py +++ /dev/null @@ -1,30 +0,0 @@ -""" -Author: Michel Peltriaux -Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany -Contact: michel.peltriaux@sgdnord.rlp.de -Created on: 24.01.22 - -""" - -from django.db.models import QuerySet - -from api.utils.v1.serializer import AbstractModelAPISerializerV1 -from intervention.models import Intervention - - -class InterventionAPISerializerV1(AbstractModelAPISerializerV1): - model = Intervention - - def compensations_to_json(self, qs: QuerySet): - return list( - qs.values( - "id", "identifier", "title" - ) - ) - - def extend_properties_data(self, entry): - self.properties_data["responsible"] = self.responsible_to_json(entry.responsible) - self.properties_data["legal"] = self.legal_to_json(entry.legal) - self.properties_data["compensations"] = self.compensations_to_json(entry.compensations.all()) - self.properties_data["payments"] = self.payments_to_json(entry.payments.all()) - self.properties_data["deductions"] = self.deductions_to_json(entry.deductions.all()) \ No newline at end of file diff --git a/api/views/v1/views.py b/api/views/v1/views.py index 688d4e0..f62aa27 100644 --- a/api/views/v1/views.py +++ b/api/views/v1/views.py @@ -5,12 +5,14 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 21.01.22 """ +import json + from django.http import JsonResponse, HttpRequest -from api.utils.v1.compensation import CompensationAPISerializerV1 -from api.utils.v1.ecoaccount import EcoAccountAPISerializerV1 -from api.utils.v1.ema import EmaAPISerializerV1 -from api.utils.v1.intervention import InterventionAPISerializerV1 +from api.utils.serializer.v1.compensation import CompensationAPISerializerV1 +from api.utils.serializer.v1.ecoaccount import EcoAccountAPISerializerV1 +from api.utils.serializer.v1.ema import EmaAPISerializerV1 +from api.utils.serializer.v1.intervention import InterventionAPISerializerV1 from api.views.views import AbstractModelAPIView @@ -19,7 +21,7 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): """ - def get(self, request: HttpRequest, id): + def get(self, request: HttpRequest, id=None): """ Handles the GET request Performs the fetching and serialization of the data @@ -32,12 +34,23 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): """ try: + if id is None: + raise AttributeError("No id provided") self.serializer.prepare_lookup(id, self.user) data = self.serializer.fetch_and_serialize() except Exception as e: return self.return_error_response(e, 500) return JsonResponse(data) + def post(self, request: HttpRequest, id=None): + try: + body = request.body.decode("utf-8") + body = json.loads(body) + created_id = self.serializer.create_model_from_json(body, self.user) + except Exception as e: + return self.return_error_response(e, 500) + return JsonResponse({"id": created_id}) + class InterventionAPIViewV1(AbstractModelAPIViewV1): serializer = InterventionAPISerializerV1 diff --git a/api/views/views.py b/api/views/views.py index 4316fb9..9608aec 100644 --- a/api/views/views.py +++ b/api/views/views.py @@ -8,6 +8,7 @@ Created on: 21.01.22 from django.http import JsonResponse from django.views import View +from django.views.decorators.csrf import csrf_exempt from api.models import APIUserToken from api.settings import KSP_TOKEN_HEADER_IDENTIFIER @@ -36,6 +37,7 @@ class AbstractModelAPIView(View): super().__init__(*args, **kwargs) self.serializer = self.serializer() + @csrf_exempt def dispatch(self, request, *args, **kwargs): try: # Fetch the proper user from the given request header token From c8dfa7e21f7bb7b19ad1a28da5890ba60b609a69 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 12:50:28 +0100 Subject: [PATCH 10/36] #31 API POST Intervention * adds check for deserializing of konova codes --- api/utils/serializer/v1/serializer.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/api/utils/serializer/v1/serializer.py b/api/utils/serializer/v1/serializer.py index 984038d..63c7182 100644 --- a/api/utils/serializer/v1/serializer.py +++ b/api/utils/serializer/v1/serializer.py @@ -63,6 +63,8 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer): Returns: """ + if json_str is None or len(json_str) == 0: + return None code = KonovaCode.objects.get( atom_id=json_str, code_lists__in=[code_list_identifier] From b87389e07b6ed1087d8f43ec4bc9845629e4400e Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 13:04:39 +0100 Subject: [PATCH 11/36] #31 API POST/PUT Intervention * splits code in smaller, reusable methods * adds put method to view * adds update_model_from_json() method --- api/utils/serializer/serializer.py | 14 +++ api/utils/serializer/v1/intervention.py | 119 +++++++++++++++++------- api/views/v1/views.py | 9 ++ 3 files changed, 107 insertions(+), 35 deletions(-) diff --git a/api/utils/serializer/serializer.py b/api/utils/serializer/serializer.py index a2b2b4b..bbe4927 100644 --- a/api/utils/serializer/serializer.py +++ b/api/utils/serializer/serializer.py @@ -78,6 +78,20 @@ class AbstractModelAPISerializer: serialized_data = self.model_to_geo_json(entry) return serialized_data + @abstractmethod + def update_model_from_json(self, id, json_model, user): + """ Updates an instance from given json data + + Args: + id (str): The instance's to be updated + json_model (dict): JSON data + user (User): The performing user + + Returns: + + """ + raise NotImplementedError("Must be implemented in subclasses") + @abstractmethod def create_model_from_json(self, json_model, user): """ Creates a new instance from given json data diff --git a/api/utils/serializer/v1/intervention.py b/api/utils/serializer/v1/intervention.py index b3ab207..65be4ea 100644 --- a/api/utils/serializer/v1/intervention.py +++ b/api/utils/serializer/v1/intervention.py @@ -34,47 +34,96 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1): self.properties_data["payments"] = self.payments_to_json(entry.payments.all()) self.properties_data["deductions"] = self.deductions_to_json(entry.deductions.all()) + def initialize_objects(self, json_model, user): + """ Initializes all needed objects from the json_model data + + Does not persist data to the DB! + + Args: + json_model (dict): The json data + user (User): The API user + + Returns: + obj (Intervention) + """ + # Create geometry + json_geom = self.create_geometry_from_json(json_model) + geometry = Geometry() + geometry.geom = json_geom + + # Create linked objects + obj = Intervention() + resp = Responsibility() + legal = Legal() + created = UserActionLogEntry.get_created_action(user, comment="API Import") + obj.legal = legal + obj.created = created + obj.geometry = geometry + obj.responsible = resp + return obj + + def set_legal(self, obj, legal_data): + """ Sets the legal data contents to the provided legal_data dict + + Args: + obj (Intervention): The intervention object + legal_data (dict): The new data + + Returns: + obj + """ + obj.legal.registration_date = legal_data["registration_date"] + obj.legal.binding_date = legal_data["binding_date"] + obj.legal.process_type = self.konova_code_from_json( + legal_data["process_type"], + CODELIST_PROCESS_TYPE_ID, + ) + laws = [self.konova_code_from_json(law, CODELIST_LAW_ID) for law in legal_data["laws"]] + obj.legal.laws.set(laws) + return obj + + def set_responsibility(self, obj, responsibility_data: dict): + """ Sets the responsible data contents to the provided responsibility_data dict + + Args: + obj (Intervention): The intervention object + responsibility_data (dict): The new data + + Returns: + obj + """ + obj.responsible.registration_office = self.konova_code_from_json( + responsibility_data["registration_office"], + CODELIST_REGISTRATION_OFFICE_ID + ) + obj.responsible.registration_file_number = responsibility_data["registration_file_number"] + obj.responsible.conservation_office = self.konova_code_from_json( + responsibility_data["conservation_office"], + CODELIST_CONSERVATION_OFFICE_ID, + ) + obj.responsible.conservation_file_number = responsibility_data["conservation_file_number"] + obj.responsible.handler = responsibility_data["handler"] + return obj + def create_model_from_json(self, json_model, user): + """ Creates a new entry for the model based on the contents of json_model + + Args: + json_model (dict): The json containing data + user (User): The API user + + Returns: + created_id (str): The id of the newly created Intervention entry + """ with transaction.atomic(): - # Create geometry - json_geom = self.create_geometry_from_json(json_model) - geometry = Geometry() - geometry.geom = json_geom - - # Create linked objects - obj = Intervention() - resp = Responsibility() - legal = Legal() - created = UserActionLogEntry.get_created_action(user, comment="API Import") - obj.legal = legal - obj.created = created - obj.geometry = geometry - obj.responsible = resp + obj = self.initialize_objects(json_model, user) # Fill in data to objects properties = json_model["properties"] obj.identifier = obj.generate_new_identifier() obj.title = properties["title"] - obj.responsible.registration_office = self.konova_code_from_json( - properties["responsible"]["registration_office"], - CODELIST_REGISTRATION_OFFICE_ID - ) - obj.responsible.registration_file_number = properties["responsible"]["registration_file_number"] - obj.responsible.conservation_office = self.konova_code_from_json( - properties["responsible"]["conservation_office"], - CODELIST_CONSERVATION_OFFICE_ID, - ) - obj.responsible.conservation_file_number = properties["responsible"]["conservation_file_number"] - obj.responsible.handler = properties["responsible"]["handler"] - - obj.legal.registration_date = properties["legal"]["registration_date"] - obj.legal.binding_date = properties["legal"]["binding_date"] - obj.legal.process_type = self.konova_code_from_json( - properties["legal"]["process_type"], - CODELIST_PROCESS_TYPE_ID, - ) - laws = [self.konova_code_from_json(law, CODELIST_LAW_ID) for law in properties["legal"]["laws"]] - obj.legal.laws.set(laws) + self.set_responsibility(obj, properties["responsible"]) + self.set_legal(obj, properties["legal"]) obj.responsible.save() obj.geometry.save() @@ -83,6 +132,6 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1): obj.users.add(user) - celery_update_parcels.delay(geometry.id) + celery_update_parcels.delay(obj.geometry.id) return obj.id diff --git a/api/views/v1/views.py b/api/views/v1/views.py index f62aa27..7e271dd 100644 --- a/api/views/v1/views.py +++ b/api/views/v1/views.py @@ -51,6 +51,15 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): return self.return_error_response(e, 500) return JsonResponse({"id": created_id}) + def put(self, request: HttpRequest, id=None): + try: + body = request.body.decode("utf-8") + body = json.loads(body) + updated_id = self.serializer.update_model_from_json(id, body, self.user) + except Exception as e: + return self.return_error_response(e, 500) + return JsonResponse({"id": updated_id}) + class InterventionAPIViewV1(AbstractModelAPIViewV1): serializer = InterventionAPISerializerV1 From 314879a1fe985fdbf31891856bd9cf275d0b4beb Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 13:12:29 +0100 Subject: [PATCH 12/36] #31 API basic implementation Cleanup * cleans code * reworks many code fragments into smaller methods and split into super class --- api/utils/serializer/serializer.py | 19 ++++++++++++++- api/utils/serializer/v1/intervention.py | 32 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/api/utils/serializer/serializer.py b/api/utils/serializer/serializer.py index bbe4927..5febafc 100644 --- a/api/utils/serializer/serializer.py +++ b/api/utils/serializer/serializer.py @@ -117,4 +117,21 @@ class AbstractModelAPISerializer: if isinstance(geojson, dict): geojson = json.dumps(geojson) geometry = geos.fromstr(geojson) - return geometry \ No newline at end of file + return geometry + + def get_obj_from_db(self, id, user): + """ Returns the object from database + + Fails if id not found or user does not have shared access + + Args: + id (str): The object's id + user (User): The API user + + Returns: + + """ + return self.model.objects.get( + id=id, + users__in=[user] + ) \ No newline at end of file diff --git a/api/utils/serializer/v1/intervention.py b/api/utils/serializer/v1/intervention.py index 65be4ea..7e5028e 100644 --- a/api/utils/serializer/v1/intervention.py +++ b/api/utils/serializer/v1/intervention.py @@ -135,3 +135,35 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1): celery_update_parcels.delay(obj.geometry.id) return obj.id + + def update_model_from_json(self, id, json_model, user): + """ Updates an entry for the model based on the contents of json_model + + Args: + id (str): The object's id + json_model (dict): The json containing data + user (User): The API user + + Returns: + created_id (str): The id of the newly created Intervention entry + """ + with transaction.atomic(): + obj = self.get_obj_from_db(id, user) + + # Fill in data to objects + properties = json_model["properties"] + obj.title = properties["title"] + self.set_responsibility(obj, properties["responsible"]) + self.set_legal(obj, properties["legal"]) + obj.geometry.geom = self.create_geometry_from_json(json_model) + + obj.responsible.save() + obj.geometry.save() + obj.legal.save() + obj.save() + + obj.users.add(user) + + celery_update_parcels.delay(obj.geometry.id) + + return obj.id From 2fa28760909a81b03033175a5503e2f246dd1495 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 14:41:56 +0100 Subject: [PATCH 13/36] #31 API POST Compensation * adds support for POST of new compensations * adds shared_users property to BaseObject and Compensation to simplify fetching of shared users (Compensation inherits from intervention) * extends compensation admin index * modifies compensation manager which led to invisibility of deleted entries in the admin backend * fixes bug in sanitize_db.py where CREATED useractions would be removed if they are not found on any log but still are used on the .created attribute of the objects --- api/utils/serializer/serializer.py | 1 - api/utils/serializer/v1/compensation.py | 207 +++++++++++++++++++++- api/utils/serializer/v1/intervention.py | 5 +- compensation/admin.py | 1 + compensation/managers.py | 4 +- compensation/models/compensation.py | 9 + konova/management/commands/sanitize_db.py | 8 +- konova/models/object.py | 12 +- 8 files changed, 237 insertions(+), 10 deletions(-) diff --git a/api/utils/serializer/serializer.py b/api/utils/serializer/serializer.py index 5febafc..c8e9083 100644 --- a/api/utils/serializer/serializer.py +++ b/api/utils/serializer/serializer.py @@ -52,7 +52,6 @@ class AbstractModelAPISerializer: """ raise NotImplementedError("Must be implemented in subclasses") - @abstractmethod def prepare_lookup(self, _id, user): """ Updates lookup dict for db fetching diff --git a/api/utils/serializer/v1/compensation.py b/api/utils/serializer/v1/compensation.py index 44d61c7..d06807e 100644 --- a/api/utils/serializer/v1/compensation.py +++ b/api/utils/serializer/v1/compensation.py @@ -5,8 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 24.01.22 """ +from django.db import transaction + from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1 -from compensation.models import Compensation +from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID +from compensation.models import Compensation, CompensationAction, CompensationState, UnitChoices +from intervention.models import Intervention +from konova.models import Geometry, Deadline +from konova.tasks import celery_update_parcels +from user.models import UserActionLogEntry class CompensationAPISerializerV1(AbstractModelAPISerializerV1): @@ -31,4 +38,200 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1): self.properties_data["before_states"] = self.compensation_state_to_json(entry.before_states.all()) self.properties_data["after_states"] = self.compensation_state_to_json(entry.after_states.all()) self.properties_data["actions"] = self.compensation_actions_to_json(entry.actions.all()) - self.properties_data["deadlines"] = self.deadlines_to_json(entry.deadlines.all()) \ No newline at end of file + self.properties_data["deadlines"] = self.deadlines_to_json(entry.deadlines.all()) + + def initialize_objects(self, json_model, user): + """ Initializes all needed objects from the json_model data + + Does not persist data to the DB! + + Args: + json_model (dict): The json data + user (User): The API user + + Returns: + obj (Compensation) + """ + create_action = UserActionLogEntry.get_created_action(user, comment="API Import") + # Create geometry + json_geom = self.create_geometry_from_json(json_model) + geometry = Geometry() + geometry.geom = json_geom + geometry.created = create_action + + # Create linked objects + obj = Compensation() + created = create_action + obj.created = created + obj.geometry = geometry + return obj + + def set_intervention(self, obj, intervention_id, user): + """ Sets the linked intervention according to the given id + + Fails if no such intervention found or user has no shared access + + Args: + obj (Compensation): The Compensation object + intervention_id (str): The intervention's id + user (User): The API user + + Returns: + obj (Compensation) + """ + intervention = Intervention.objects.get( + id=intervention_id, + users__in=[user], + ) + obj.intervention = intervention + return obj + + def set_deadlines(self, obj, deadline_data): + found_deadlines = [] + for entry in deadline_data: + deadline_type = entry["type"] + date = entry["date"] + comment = entry["comment"] + + pre_existing_deadlines = obj.deadlines.filter( + type=deadline_type, + date=date, + comment=comment, + ).exclude( + id__in=found_deadlines + ) + if pre_existing_deadlines.count() > 0: + found_deadlines += pre_existing_deadlines.values_list("id", flat=True) + else: + # Create! + new_deadline = Deadline.objects.create( + type=deadline_type, + date=date, + comment=comment, + ) + obj.deadlines.add(new_deadline) + return obj + + def set_compensation_states(self, obj, states_data, states_manager): + found_states = [] + for entry in states_data: + biotope_type = entry["biotope"] + surface = float(entry["surface"]) + if surface <= 0: + raise ValueError("State surfaces must be > 0") + pre_existing_states = states_manager.filter( + biotope_type__atom_id=biotope_type, + surface=surface, + ).exclude( + id__in=found_states + ) + if pre_existing_states.count() > 0: + found_states += pre_existing_states.values_list("id", flat=True) + else: + # Create! + new_state = CompensationState.objects.create( + biotope_type=self.konova_code_from_json(biotope_type, CODELIST_BIOTOPES_ID), + surface=surface + ) + states_manager.add(new_state) + return obj + + def set_compensation_actions(self, obj, actions_data): + found_actions = [] + for entry in actions_data: + action = entry["action"] + amount = float(entry["amount"]) + unit = entry["unit"] + comment = entry["comment"] + + if amount <= 0: + raise ValueError("Action amount must be > 0") + if unit not in UnitChoices: + raise ValueError(f"Invalid unit. Choices are {UnitChoices.values}") + pre_existing_actions = obj.actions.filter( + action_type__atom_id=action, + amount=amount, + unit=unit, + comment=comment, + ).exclude( + id__in=found_actions + ) + if pre_existing_actions.count() > 0: + found_actions += pre_existing_actions.values_list("id", flat=True) + else: + # Create! + new_action = CompensationAction.objects.create( + action_type=self.konova_code_from_json(action, CODELIST_COMPENSATION_ACTION_ID), + amount=amount, + unit=unit, + comment=comment, + ) + obj.actions.add(new_action) + return obj + + def create_model_from_json(self, json_model, user): + """ Creates a new entry for the model based on the contents of json_model + + Args: + json_model (dict): The json containing data + user (User): The API user + + Returns: + created_id (str): The id of the newly created Compensation entry + """ + with transaction.atomic(): + obj = self.initialize_objects(json_model, user) + + # Fill in data to objects + properties = json_model["properties"] + obj.identifier = obj.generate_new_identifier() + obj.title = properties["title"] + obj.is_cef = properties["is_cef"] + obj.is_coherence_keeping = properties["is_coherence_keeping"] + obj = self.set_intervention(obj, properties["intervention"], user) + + obj.geometry.save() + obj.save() + + obj = self.set_compensation_actions(obj, properties["actions"]) + obj = self.set_compensation_states(obj, properties["before_states"], obj.before_states) + obj = self.set_compensation_states(obj, properties["after_states"], obj.after_states) + obj = self.set_deadlines(obj, properties["deadlines"]) + + obj.log.add(obj.created) + + celery_update_parcels.delay(obj.geometry.id) + + return obj.id + + def update_model_from_json(self, id, json_model, user): + """ Updates an entry for the model based on the contents of json_model + + Args: + id (str): The object's id + json_model (dict): The json containing data + user (User): The API user + + Returns: + created_id (str): The id of the newly created Compensation entry + """ + with transaction.atomic(): + obj = self.get_obj_from_db(id, user) + + # Fill in data to objects + properties = json_model["properties"] + obj.title = properties["title"] + self.set_responsibility(obj, properties["responsible"]) + self.set_legal(obj, properties["legal"]) + obj.geometry.geom = self.create_geometry_from_json(json_model) + + obj.responsible.save() + obj.geometry.save() + obj.legal.save() + obj.save() + + obj.users.add(user) + + celery_update_parcels.delay(obj.geometry.id) + + return obj.id diff --git a/api/utils/serializer/v1/intervention.py b/api/utils/serializer/v1/intervention.py index 7e5028e..8264408 100644 --- a/api/utils/serializer/v1/intervention.py +++ b/api/utils/serializer/v1/intervention.py @@ -46,16 +46,18 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1): Returns: obj (Intervention) """ + create_action = UserActionLogEntry.get_created_action(user, comment="API Import") # Create geometry json_geom = self.create_geometry_from_json(json_model) geometry = Geometry() geometry.geom = json_geom + geometry.created = create_action # Create linked objects obj = Intervention() resp = Responsibility() legal = Legal() - created = UserActionLogEntry.get_created_action(user, comment="API Import") + created = create_action obj.legal = legal obj.created = created obj.geometry = geometry @@ -131,6 +133,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1): obj.save() obj.users.add(user) + obj.log.add(obj.created) celery_update_parcels.delay(obj.geometry.id) diff --git a/compensation/admin.py b/compensation/admin.py index ced9bfe..735ffb4 100644 --- a/compensation/admin.py +++ b/compensation/admin.py @@ -29,6 +29,7 @@ class CompensationAdmin(BaseObjectAdmin): "identifier", "title", "created", + "deleted", ] diff --git a/compensation/managers.py b/compensation/managers.py index 61933ee..c97cd51 100644 --- a/compensation/managers.py +++ b/compensation/managers.py @@ -35,9 +35,7 @@ class CompensationManager(models.Manager): """ def get_queryset(self): - return super().get_queryset().filter( - deleted__isnull=True, - ).select_related( + return super().get_queryset().select_related( "modified", "intervention", "intervention__recorded", diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index 6ebed85..59a3fd9 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -245,6 +245,15 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): # Compensations inherit their shared state from the interventions return self.intervention.is_shared_with(user) + @property + def shared_users(self) -> QuerySet: + """ Shortcut for fetching the users which have shared access on this object + + Returns: + users (QuerySet) + """ + return self.intervention.users.all() + def get_LANIS_link(self) -> str: """ Generates a link for LANIS depending on the geometry diff --git a/konova/management/commands/sanitize_db.py b/konova/management/commands/sanitize_db.py index 0eecdc0..b5be834 100644 --- a/konova/management/commands/sanitize_db.py +++ b/konova/management/commands/sanitize_db.py @@ -10,7 +10,7 @@ from ema.models import Ema from intervention.models import Intervention from konova.management.commands.setup import BaseKonovaCommand from konova.models import Deadline, Geometry, Parcel, District -from user.models import UserActionLogEntry +from user.models import UserActionLogEntry, UserAction class Command(BaseKonovaCommand): @@ -55,7 +55,11 @@ class Command(BaseKonovaCommand): """ self._write_warning("=== Sanitize log entries ===") - all_log_entries = UserActionLogEntry.objects.all() + # Exclude created log entries from being cleaned, since they can be part of objects which do not have logs + # Being in a log (or not) is essential for this cleanup + all_log_entries = UserActionLogEntry.objects.all().exclude( + action=UserAction.CREATED + ) intervention_log_entries_ids = self.get_all_log_entries_ids(Intervention) attached_log_entries_id = intervention_log_entries_ids.union( diff --git a/konova/models/object.py b/konova/models/object.py index 0a83a48..5a7d6f1 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -10,6 +10,7 @@ import uuid from abc import abstractmethod from django.contrib import messages +from django.db.models import QuerySet from konova.tasks import celery_send_mail_shared_access_removed, celery_send_mail_shared_access_given, \ celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \ @@ -124,7 +125,7 @@ class BaseObject(BaseResource): self.log.add(action) # Send mail - shared_users = self.users.all().values_list("id", flat=True) + shared_users = self.shared_users.values_list("id", flat=True) for user_id in shared_users: celery_send_mail_shared_data_deleted.delay(self.identifier, user_id) @@ -464,6 +465,15 @@ class ShareableObjectMixin(models.Model): # Set new shared users self.share_with_list(users) + @property + def shared_users(self) -> QuerySet: + """ Shortcut for fetching the users which have shared access on this object + + Returns: + users (QuerySet) + """ + return self.users.all() + class GeoReferencedMixin(models.Model): geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL) From 79acf63dbffbbb4a8ed5bd0a5530d944bcc7515a Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 14:51:50 +0100 Subject: [PATCH 14/36] #31 API POST Compensation * adds initialize_objects to an abstractmethod of the super class to be implemented in subclasses * differentiates error messages if intervention does not exist or is just not shared with the user --- api/utils/serializer/serializer.py | 17 ++++++++++++++++- api/utils/serializer/v1/compensation.py | 4 +++- 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/api/utils/serializer/serializer.py b/api/utils/serializer/serializer.py index c8e9083..3820b30 100644 --- a/api/utils/serializer/serializer.py +++ b/api/utils/serializer/serializer.py @@ -133,4 +133,19 @@ class AbstractModelAPISerializer: return self.model.objects.get( id=id, users__in=[user] - ) \ No newline at end of file + ) + + @abstractmethod + def initialize_objects(self, json_model, user): + """ Initializes all needed objects from the json_model data + + Does not persist data to the DB! + + Args: + json_model (dict): The json data + user (User): The API user + + Returns: + obj (Intervention) + """ + raise NotImplementedError("Must be implemented in subclasses") diff --git a/api/utils/serializer/v1/compensation.py b/api/utils/serializer/v1/compensation.py index d06807e..3cc2261 100644 --- a/api/utils/serializer/v1/compensation.py +++ b/api/utils/serializer/v1/compensation.py @@ -81,8 +81,10 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1): """ intervention = Intervention.objects.get( id=intervention_id, - users__in=[user], ) + is_shared = intervention.is_shared_with(user) + if not is_shared: + raise PermissionError("Intervention not shared with user") obj.intervention = intervention return obj From 02e72e015f64b763c8308630e511c023fa668197 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 15:04:20 +0100 Subject: [PATCH 15/36] #31 API POST Compensation * adds documentations * adds check for valid deadline type --- api/utils/serializer/v1/compensation.py | 38 +++++++++++++++++++++++-- 1 file changed, 36 insertions(+), 2 deletions(-) diff --git a/api/utils/serializer/v1/compensation.py b/api/utils/serializer/v1/compensation.py index 3cc2261..6fb8486 100644 --- a/api/utils/serializer/v1/compensation.py +++ b/api/utils/serializer/v1/compensation.py @@ -11,7 +11,7 @@ from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1 from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID from compensation.models import Compensation, CompensationAction, CompensationState, UnitChoices from intervention.models import Intervention -from konova.models import Geometry, Deadline +from konova.models import Geometry, Deadline, DeadlineType from konova.tasks import celery_update_parcels from user.models import UserActionLogEntry @@ -67,7 +67,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1): return obj def set_intervention(self, obj, intervention_id, user): - """ Sets the linked intervention according to the given id + """ Sets the linked compensation according to the given id Fails if no such intervention found or user has no shared access @@ -89,12 +89,25 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1): return obj def set_deadlines(self, obj, deadline_data): + """ Sets the linked deadline data according to the given deadline_data + + + Args: + obj (Compensation): The Compensation object + deadline_data (dict): The posted deadline_data + + Returns: + obj (Compensation) + """ found_deadlines = [] for entry in deadline_data: deadline_type = entry["type"] date = entry["date"] comment = entry["comment"] + if deadline_type not in DeadlineType: + raise ValueError(f"Invalid deadline type. Choices are {DeadlineType.values}") + pre_existing_deadlines = obj.deadlines.filter( type=deadline_type, date=date, @@ -115,6 +128,17 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1): return obj def set_compensation_states(self, obj, states_data, states_manager): + """ Sets the linked compensation state data according to the given states_data + + + Args: + obj (Compensation): The Compensation object + states_data (dict): The posted states_data + states_manager (Manager): The before_states or after_states manager + + Returns: + obj (Compensation) + """ found_states = [] for entry in states_data: biotope_type = entry["biotope"] @@ -139,6 +163,16 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1): return obj def set_compensation_actions(self, obj, actions_data): + """ Sets the linked compensation action data according to the given actions_data + + + Args: + obj (Compensation): The Compensation object + actions_data (dict): The posted actions_data + + Returns: + obj (Compensation) + """ found_actions = [] for entry in actions_data: action = entry["action"] From 07331078c432e098c7afbf35ed3a93427110d59f Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 15:20:23 +0100 Subject: [PATCH 16/36] #31 API code cleaning * splits large AbstractModelAPISerializer into different reusable Mixins to increase reusability of code for similar models --- api/utils/serializer/v1/compensation.py | 125 +------------ api/utils/serializer/v1/ecoaccount.py | 9 +- api/utils/serializer/v1/ema.py | 5 +- api/utils/serializer/v1/intervention.py | 16 +- api/utils/serializer/v1/serializer.py | 231 ++++++++++++++++++------ 5 files changed, 208 insertions(+), 178 deletions(-) diff --git a/api/utils/serializer/v1/compensation.py b/api/utils/serializer/v1/compensation.py index 6fb8486..0578779 100644 --- a/api/utils/serializer/v1/compensation.py +++ b/api/utils/serializer/v1/compensation.py @@ -7,16 +7,15 @@ Created on: 24.01.22 """ from django.db import transaction -from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1 -from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID -from compensation.models import Compensation, CompensationAction, CompensationState, UnitChoices +from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin +from compensation.models import Compensation from intervention.models import Intervention -from konova.models import Geometry, Deadline, DeadlineType +from konova.models import Geometry from konova.tasks import celery_update_parcels from user.models import UserActionLogEntry -class CompensationAPISerializerV1(AbstractModelAPISerializerV1): +class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin): model = Compensation def prepare_lookup(self, id, user): @@ -88,122 +87,6 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1): obj.intervention = intervention return obj - def set_deadlines(self, obj, deadline_data): - """ Sets the linked deadline data according to the given deadline_data - - - Args: - obj (Compensation): The Compensation object - deadline_data (dict): The posted deadline_data - - Returns: - obj (Compensation) - """ - found_deadlines = [] - for entry in deadline_data: - deadline_type = entry["type"] - date = entry["date"] - comment = entry["comment"] - - if deadline_type not in DeadlineType: - raise ValueError(f"Invalid deadline type. Choices are {DeadlineType.values}") - - pre_existing_deadlines = obj.deadlines.filter( - type=deadline_type, - date=date, - comment=comment, - ).exclude( - id__in=found_deadlines - ) - if pre_existing_deadlines.count() > 0: - found_deadlines += pre_existing_deadlines.values_list("id", flat=True) - else: - # Create! - new_deadline = Deadline.objects.create( - type=deadline_type, - date=date, - comment=comment, - ) - obj.deadlines.add(new_deadline) - return obj - - def set_compensation_states(self, obj, states_data, states_manager): - """ Sets the linked compensation state data according to the given states_data - - - Args: - obj (Compensation): The Compensation object - states_data (dict): The posted states_data - states_manager (Manager): The before_states or after_states manager - - Returns: - obj (Compensation) - """ - found_states = [] - for entry in states_data: - biotope_type = entry["biotope"] - surface = float(entry["surface"]) - if surface <= 0: - raise ValueError("State surfaces must be > 0") - pre_existing_states = states_manager.filter( - biotope_type__atom_id=biotope_type, - surface=surface, - ).exclude( - id__in=found_states - ) - if pre_existing_states.count() > 0: - found_states += pre_existing_states.values_list("id", flat=True) - else: - # Create! - new_state = CompensationState.objects.create( - biotope_type=self.konova_code_from_json(biotope_type, CODELIST_BIOTOPES_ID), - surface=surface - ) - states_manager.add(new_state) - return obj - - def set_compensation_actions(self, obj, actions_data): - """ Sets the linked compensation action data according to the given actions_data - - - Args: - obj (Compensation): The Compensation object - actions_data (dict): The posted actions_data - - Returns: - obj (Compensation) - """ - found_actions = [] - for entry in actions_data: - action = entry["action"] - amount = float(entry["amount"]) - unit = entry["unit"] - comment = entry["comment"] - - if amount <= 0: - raise ValueError("Action amount must be > 0") - if unit not in UnitChoices: - raise ValueError(f"Invalid unit. Choices are {UnitChoices.values}") - pre_existing_actions = obj.actions.filter( - action_type__atom_id=action, - amount=amount, - unit=unit, - comment=comment, - ).exclude( - id__in=found_actions - ) - if pre_existing_actions.count() > 0: - found_actions += pre_existing_actions.values_list("id", flat=True) - else: - # Create! - new_action = CompensationAction.objects.create( - action_type=self.konova_code_from_json(action, CODELIST_COMPENSATION_ACTION_ID), - amount=amount, - unit=unit, - comment=comment, - ) - obj.actions.add(new_action) - return obj def create_model_from_json(self, json_model, user): """ Creates a new entry for the model based on the contents of json_model diff --git a/api/utils/serializer/v1/ecoaccount.py b/api/utils/serializer/v1/ecoaccount.py index 2491eb1..5ddbffd 100644 --- a/api/utils/serializer/v1/ecoaccount.py +++ b/api/utils/serializer/v1/ecoaccount.py @@ -5,12 +5,17 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 24.01.22 """ -from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1 +from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, \ + LegalAPISerializerV1Mixin, ResponsibilityAPISerializerV1Mixin, DeductableAPISerializerV1Mixin from compensation.models import EcoAccount from intervention.models import Legal, Responsibility -class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1): +class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1, + AbstractCompensationAPISerializerV1Mixin, + LegalAPISerializerV1Mixin, + ResponsibilityAPISerializerV1Mixin, + DeductableAPISerializerV1Mixin): model = EcoAccount def extend_properties_data(self, entry): diff --git a/api/utils/serializer/v1/ema.py b/api/utils/serializer/v1/ema.py index e8f2228..a2e0bc1 100644 --- a/api/utils/serializer/v1/ema.py +++ b/api/utils/serializer/v1/ema.py @@ -5,12 +5,13 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 24.01.22 """ -from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1 +from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, \ + ResponsibilityAPISerializerV1Mixin from ema.models import Ema from intervention.models import Responsibility -class EmaAPISerializerV1(AbstractModelAPISerializerV1): +class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, ResponsibilityAPISerializerV1Mixin): model = Ema def responsible_to_json(self, responsible: Responsibility): diff --git a/api/utils/serializer/v1/intervention.py b/api/utils/serializer/v1/intervention.py index 8264408..1b91a6c 100644 --- a/api/utils/serializer/v1/intervention.py +++ b/api/utils/serializer/v1/intervention.py @@ -8,7 +8,8 @@ Created on: 24.01.22 from django.db import transaction from django.db.models import QuerySet -from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1 +from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, \ + ResponsibilityAPISerializerV1Mixin, LegalAPISerializerV1Mixin from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID, \ CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID from intervention.models import Intervention, Responsibility, Legal @@ -17,7 +18,7 @@ from konova.tasks import celery_update_parcels from user.models import UserActionLogEntry -class InterventionAPISerializerV1(AbstractModelAPISerializerV1): +class InterventionAPISerializerV1(AbstractModelAPISerializerV1, ResponsibilityAPISerializerV1Mixin, LegalAPISerializerV1Mixin): model = Intervention def compensations_to_json(self, qs: QuerySet): @@ -27,6 +28,17 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1): ) ) + def payments_to_json(self, qs: QuerySet): + """ Serializes payments into json + + Args: + qs (QuerySet): A queryset of Payment entries + + Returns: + serialized_json (list) + """ + return list(qs.values("amount", "due_on", "comment")) + def extend_properties_data(self, entry): self.properties_data["responsible"] = self.responsible_to_json(entry.responsible) self.properties_data["legal"] = self.legal_to_json(entry.legal) diff --git a/api/utils/serializer/v1/serializer.py b/api/utils/serializer/v1/serializer.py index 63c7182..f60428b 100644 --- a/api/utils/serializer/v1/serializer.py +++ b/api/utils/serializer/v1/serializer.py @@ -12,7 +12,10 @@ from django.db.models import QuerySet from api.utils.serializer.serializer import AbstractModelAPISerializer from codelist.models import KonovaCode +from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID +from compensation.models import CompensationAction, UnitChoices, CompensationState from intervention.models import Responsibility, Legal +from konova.models import Deadline, DeadlineType class AbstractModelAPISerializerV1(AbstractModelAPISerializer): @@ -71,6 +74,67 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer): ) return code + def created_on_to_json(self, entry): + """ Serializes the created_on into json + + Args: + entry (BaseObject): The entry + + Returns: + created_on (timestamp) + """ + return entry.created.timestamp + + def modified_on_to_json(self, entry): + """ Serializes the modified_on into json + + Args: + entry (BaseObject): The entry + + Returns: + modified_on (timestamp) + """ + modified_on = entry.modified or entry.created + modified_on = modified_on.timestamp + return modified_on + + +class DeductableAPISerializerV1Mixin: + class Meta: + abstract = True + + def deductions_to_json(self, qs: QuerySet): + """ Serializes eco account deductions into json + + Args: + qs (QuerySet): A queryset of EcoAccountDeduction entries + + Returns: + serialized_json (list) + """ + return [ + { + "id": entry.pk, + "eco_account": { + "id": entry.account.pk, + "identifier": entry.account.identifier, + "title": entry.account.title, + }, + "surface": entry.surface, + "intervention": { + "id": entry.intervention.pk, + "identifier": entry.intervention.identifier, + "title": entry.intervention.title, + } + } + for entry in qs + ] + + +class ResponsibilityAPISerializerV1Mixin: + class Meta: + abstract = True + def responsible_to_json(self, responsible: Responsibility): """ Serializes Responsibility model into json @@ -88,6 +152,11 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer): "handler": responsible.handler, } + +class LegalAPISerializerV1Mixin: + class Meta: + abstract = True + def legal_to_json(self, legal: Legal): """ Serializes Legal model into json @@ -104,43 +173,127 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer): "laws": [self.konova_code_to_json(law) for law in legal.laws.all()], } - def payments_to_json(self, qs: QuerySet): - """ Serializes payments into json + +class AbstractCompensationAPISerializerV1Mixin: + class Meta: + abstract = True + + def set_deadlines(self, obj, deadline_data): + """ Sets the linked deadline data according to the given deadline_data + Args: - qs (QuerySet): A queryset of Payment entries + obj (Compensation): The Compensation object + deadline_data (dict): The posted deadline_data Returns: - serialized_json (list) + obj (Compensation) """ - return list(qs.values("amount", "due_on", "comment")) + found_deadlines = [] + for entry in deadline_data: + deadline_type = entry["type"] + date = entry["date"] + comment = entry["comment"] + + if deadline_type not in DeadlineType: + raise ValueError(f"Invalid deadline type. Choices are {DeadlineType.values}") + + pre_existing_deadlines = obj.deadlines.filter( + type=deadline_type, + date=date, + comment=comment, + ).exclude( + id__in=found_deadlines + ) + if pre_existing_deadlines.count() > 0: + found_deadlines += pre_existing_deadlines.values_list("id", flat=True) + else: + # Create! + new_deadline = Deadline.objects.create( + type=deadline_type, + date=date, + comment=comment, + ) + obj.deadlines.add(new_deadline) + return obj + + def set_compensation_states(self, obj, states_data, states_manager): + """ Sets the linked compensation state data according to the given states_data - def deductions_to_json(self, qs: QuerySet): - """ Serializes eco account deductions into json Args: - qs (QuerySet): A queryset of EcoAccountDeduction entries + obj (Compensation): The Compensation object + states_data (dict): The posted states_data + states_manager (Manager): The before_states or after_states manager Returns: - serialized_json (list) + obj (Compensation) """ - return [ - { - "id": entry.pk, - "eco_account": { - "id": entry.account.pk, - "identifier": entry.account.identifier, - "title": entry.account.title, - }, - "surface": entry.surface, - "intervention": { - "id": entry.intervention.pk, - "identifier": entry.intervention.identifier, - "title": entry.intervention.title, - } - } - for entry in qs - ] + found_states = [] + for entry in states_data: + biotope_type = entry["biotope"] + surface = float(entry["surface"]) + if surface <= 0: + raise ValueError("State surfaces must be > 0") + pre_existing_states = states_manager.filter( + biotope_type__atom_id=biotope_type, + surface=surface, + ).exclude( + id__in=found_states + ) + if pre_existing_states.count() > 0: + found_states += pre_existing_states.values_list("id", flat=True) + else: + # Create! + new_state = CompensationState.objects.create( + biotope_type=self.konova_code_from_json(biotope_type, CODELIST_BIOTOPES_ID), + surface=surface + ) + states_manager.add(new_state) + return obj + + def set_compensation_actions(self, obj, actions_data): + """ Sets the linked compensation action data according to the given actions_data + + + Args: + obj (Compensation): The Compensation object + actions_data (dict): The posted actions_data + + Returns: + obj (Compensation) + """ + found_actions = [] + for entry in actions_data: + action = entry["action"] + amount = float(entry["amount"]) + unit = entry["unit"] + comment = entry["comment"] + + if amount <= 0: + raise ValueError("Action amount must be > 0") + if unit not in UnitChoices: + raise ValueError(f"Invalid unit. Choices are {UnitChoices.values}") + pre_existing_actions = obj.actions.filter( + action_type__atom_id=action, + amount=amount, + unit=unit, + comment=comment, + ).exclude( + id__in=found_actions + ) + if pre_existing_actions.count() > 0: + found_actions += pre_existing_actions.values_list("id", flat=True) + else: + # Create! + new_action = CompensationAction.objects.create( + action_type=self.konova_code_from_json(action, CODELIST_COMPENSATION_ACTION_ID), + amount=amount, + unit=unit, + comment=comment, + ) + obj.actions.add(new_action) + return obj def compensation_state_to_json(self, qs: QuerySet): """ Serializes compensation states into json @@ -191,28 +344,4 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer): "type", "date", "comment", - )) - - def created_on_to_json(self, entry): - """ Serializes the created_on into json - - Args: - entry (BaseObject): The entry - - Returns: - created_on (timestamp) - """ - return entry.created.timestamp - - def modified_on_to_json(self, entry): - """ Serializes the modified_on into json - - Args: - entry (BaseObject): The entry - - Returns: - modified_on (timestamp) - """ - modified_on = entry.modified or entry.created - modified_on = modified_on.timestamp - return modified_on + )) \ No newline at end of file From d58ca3f324829a6d1c716bcf3599db3f2da84efb Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 15:56:02 +0100 Subject: [PATCH 17/36] #31 API PUT Compensation * adds support for PUT compensation (Update) * improves updating of related objects * adds missing payment PUT support for intervention API --- api/utils/serializer/v1/compensation.py | 41 ++++++++++++--- api/utils/serializer/v1/intervention.py | 66 +++++++++++++++++++++++-- api/utils/serializer/v1/serializer.py | 64 +++++++++++++++--------- 3 files changed, 138 insertions(+), 33 deletions(-) diff --git a/api/utils/serializer/v1/compensation.py b/api/utils/serializer/v1/compensation.py index 0578779..e2ec260 100644 --- a/api/utils/serializer/v1/compensation.py +++ b/api/utils/serializer/v1/compensation.py @@ -78,16 +78,21 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa Returns: obj (Compensation) """ + if obj.intervention.id == intervention_id: + # Nothing to do here + return obj + intervention = Intervention.objects.get( id=intervention_id, ) is_shared = intervention.is_shared_with(user) + if not is_shared: raise PermissionError("Intervention not shared with user") + obj.intervention = intervention return obj - def create_model_from_json(self, json_model, user): """ Creates a new entry for the model based on the contents of json_model @@ -123,6 +128,23 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa return obj.id + def get_obj_from_db(self, id, user): + """ Returns the object from database + + Fails if id not found or user does not have shared access + + Args: + id (str): The object's id + user (User): The API user + + Returns: + + """ + return self.model.objects.get( + id=id, + intervention__users__in=[user] + ) + def update_model_from_json(self, id, json_model, user): """ Updates an entry for the model based on the contents of json_model @@ -135,21 +157,28 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa created_id (str): The id of the newly created Compensation entry """ with transaction.atomic(): + update_action = UserActionLogEntry.get_edited_action(user, "API update") obj = self.get_obj_from_db(id, user) # Fill in data to objects properties = json_model["properties"] obj.title = properties["title"] - self.set_responsibility(obj, properties["responsible"]) - self.set_legal(obj, properties["legal"]) + obj.is_cef = properties["is_cef"] + obj.is_coherence_keeping = properties["is_coherence_keeping"] + obj.modified = update_action obj.geometry.geom = self.create_geometry_from_json(json_model) + obj.geometry.modified = update_action + obj = self.set_intervention(obj, properties["intervention"], user) - obj.responsible.save() obj.geometry.save() - obj.legal.save() obj.save() - obj.users.add(user) + obj = self.set_compensation_actions(obj, properties["actions"]) + obj = self.set_compensation_states(obj, properties["before_states"], obj.before_states) + obj = self.set_compensation_states(obj, properties["after_states"], obj.after_states) + obj = self.set_deadlines(obj, properties["deadlines"]) + + obj.log.add(update_action) celery_update_parcels.delay(obj.geometry.id) diff --git a/api/utils/serializer/v1/intervention.py b/api/utils/serializer/v1/intervention.py index 1b91a6c..34fb75c 100644 --- a/api/utils/serializer/v1/intervention.py +++ b/api/utils/serializer/v1/intervention.py @@ -9,16 +9,20 @@ from django.db import transaction from django.db.models import QuerySet from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, \ - ResponsibilityAPISerializerV1Mixin, LegalAPISerializerV1Mixin + ResponsibilityAPISerializerV1Mixin, LegalAPISerializerV1Mixin, DeductableAPISerializerV1Mixin from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID, \ CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID +from compensation.models import Payment from intervention.models import Intervention, Responsibility, Legal from konova.models import Geometry from konova.tasks import celery_update_parcels from user.models import UserActionLogEntry -class InterventionAPISerializerV1(AbstractModelAPISerializerV1, ResponsibilityAPISerializerV1Mixin, LegalAPISerializerV1Mixin): +class InterventionAPISerializerV1(AbstractModelAPISerializerV1, + ResponsibilityAPISerializerV1Mixin, + LegalAPISerializerV1Mixin, + DeductableAPISerializerV1Mixin): model = Intervention def compensations_to_json(self, qs: QuerySet): @@ -119,6 +123,58 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1, ResponsibilityAP obj.responsible.handler = responsibility_data["handler"] return obj + def set_payments(self, obj, payment_data): + """ Sets the linked Payment data according to the given payment_data + + + Args: + obj (Compensation): The Compensation object + payment_data (dict): The posted payment_data + + Returns: + obj (intervention) + """ + payments = [] + for entry in payment_data: + due_on = entry["due_on"] + amount = float(entry["amount"]) + comment = entry["comment"] + + # Check on validity + if amount <= 0: + raise ValueError("Payment amount must be > 0") + + no_due_on = due_on is None or len(due_on) == 0 + no_comment = comment is None or len(comment) == 0 + + if no_due_on and no_comment: + raise ValueError("If no due_on can be provided, you need to explain why using the comment") + + # If this exact data is already existing, we do not create it new. Instead put it's id in the list of + # entries, we will use to set the new actions + pre_existing_payment = obj.payments.filter( + amount=amount, + due_on=due_on, + comment=comment, + ).exclude( + id__in=payments + ).first() + if pre_existing_payment is not None: + payments.append(pre_existing_payment.id) + else: + # Create and add id to list + new_payment = Payment.objects.create( + amount=amount, + due_on=due_on, + comment=comment, + ) + payments.append(new_payment.id) + payments = Payment.objects.filter( + id__in=payments + ) + obj.payments.set(payments) + return obj + def create_model_from_json(self, json_model, user): """ Creates a new entry for the model based on the contents of json_model @@ -163,21 +219,25 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1, ResponsibilityAP created_id (str): The id of the newly created Intervention entry """ with transaction.atomic(): + update_action = UserActionLogEntry.get_edited_action(user, "API update") obj = self.get_obj_from_db(id, user) # Fill in data to objects properties = json_model["properties"] obj.title = properties["title"] + obj.modified = update_action self.set_responsibility(obj, properties["responsible"]) self.set_legal(obj, properties["legal"]) + self.set_payments(obj, properties["payments"]) obj.geometry.geom = self.create_geometry_from_json(json_model) + obj.geometry.modified = update_action obj.responsible.save() obj.geometry.save() obj.legal.save() obj.save() - obj.users.add(user) + obj.log.add(update_action) celery_update_parcels.delay(obj.geometry.id) diff --git a/api/utils/serializer/v1/serializer.py b/api/utils/serializer/v1/serializer.py index f60428b..af3fa96 100644 --- a/api/utils/serializer/v1/serializer.py +++ b/api/utils/serializer/v1/serializer.py @@ -189,32 +189,36 @@ class AbstractCompensationAPISerializerV1Mixin: Returns: obj (Compensation) """ - found_deadlines = [] + deadlines = [] for entry in deadline_data: deadline_type = entry["type"] date = entry["date"] comment = entry["comment"] + # Check on validity if deadline_type not in DeadlineType: raise ValueError(f"Invalid deadline type. Choices are {DeadlineType.values}") - pre_existing_deadlines = obj.deadlines.filter( + # If this exact data is already existing, we do not create it new. Instead put it's id in the list of + # entries, we will use to set the new actions + pre_existing_deadline = obj.deadlines.filter( type=deadline_type, date=date, comment=comment, ).exclude( - id__in=found_deadlines - ) - if pre_existing_deadlines.count() > 0: - found_deadlines += pre_existing_deadlines.values_list("id", flat=True) + id__in=deadlines + ).first() + if pre_existing_deadline is not None: + deadlines.append(pre_existing_deadline.id) else: - # Create! + # Create and add id to list new_deadline = Deadline.objects.create( type=deadline_type, date=date, comment=comment, ) - obj.deadlines.add(new_deadline) + deadlines.append(new_deadline.id) + obj.deadlines.set(deadlines) return obj def set_compensation_states(self, obj, states_data, states_manager): @@ -229,27 +233,34 @@ class AbstractCompensationAPISerializerV1Mixin: Returns: obj (Compensation) """ - found_states = [] + states = [] for entry in states_data: biotope_type = entry["biotope"] surface = float(entry["surface"]) + + # Check on validity if surface <= 0: raise ValueError("State surfaces must be > 0") - pre_existing_states = states_manager.filter( + + # If this exact data is already existing, we do not create it new. Instead put it's id in the list of + # entries, we will use to set the new actions + pre_existing_state = states_manager.filter( biotope_type__atom_id=biotope_type, surface=surface, ).exclude( - id__in=found_states - ) - if pre_existing_states.count() > 0: - found_states += pre_existing_states.values_list("id", flat=True) + id__in=states + ).first() + if pre_existing_state is not None: + states.append(pre_existing_state.id) else: - # Create! + # Create and add id to list new_state = CompensationState.objects.create( biotope_type=self.konova_code_from_json(biotope_type, CODELIST_BIOTOPES_ID), surface=surface ) - states_manager.add(new_state) + states.append(new_state.id) + + states_manager.set(states) return obj def set_compensation_actions(self, obj, actions_data): @@ -263,36 +274,41 @@ class AbstractCompensationAPISerializerV1Mixin: Returns: obj (Compensation) """ - found_actions = [] + actions = [] for entry in actions_data: action = entry["action"] amount = float(entry["amount"]) unit = entry["unit"] comment = entry["comment"] + # Check on validity if amount <= 0: raise ValueError("Action amount must be > 0") if unit not in UnitChoices: raise ValueError(f"Invalid unit. Choices are {UnitChoices.values}") - pre_existing_actions = obj.actions.filter( + + # If this exact data is already existing, we do not create it new. Instead put it's id in the list of + # entries, we will use to set the new actions + pre_existing_action = obj.actions.filter( action_type__atom_id=action, amount=amount, unit=unit, comment=comment, ).exclude( - id__in=found_actions - ) - if pre_existing_actions.count() > 0: - found_actions += pre_existing_actions.values_list("id", flat=True) + id__in=actions + ).first() + if pre_existing_action is not None: + actions.append(pre_existing_action.id) else: - # Create! + # Create and add id to list new_action = CompensationAction.objects.create( action_type=self.konova_code_from_json(action, CODELIST_COMPENSATION_ACTION_ID), amount=amount, unit=unit, comment=comment, ) - obj.actions.add(new_action) + actions.append(new_action.id) + obj.actions.set(actions) return obj def compensation_state_to_json(self, qs: QuerySet): From f461a8e38d91c920ee2d571c70e6cb3510d43c5d Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 16:23:38 +0100 Subject: [PATCH 18/36] #31 API Improvement * adds support for returning all shared data * adds documentation --- api/utils/serializer/serializer.py | 13 ++++++++--- api/utils/serializer/v1/compensation.py | 2 +- api/utils/serializer/v1/intervention.py | 12 +++++++--- api/utils/serializer/v1/serializer.py | 6 +++-- api/views/v1/views.py | 29 ++++++++++++++++++++----- 5 files changed, 48 insertions(+), 14 deletions(-) diff --git a/api/utils/serializer/serializer.py b/api/utils/serializer/serializer.py index 3820b30..2a7639b 100644 --- a/api/utils/serializer/serializer.py +++ b/api/utils/serializer/serializer.py @@ -62,7 +62,12 @@ class AbstractModelAPISerializer: Returns: """ - self.lookup["id"] = _id + if _id is None: + # Return all objects + del self.lookup["id"] + else: + # Return certain objects + self.lookup["id"] = _id self.lookup["users__in"] = [user] def fetch_and_serialize(self): @@ -73,8 +78,10 @@ class AbstractModelAPISerializer: Returns: serialized_data (dict) """ - entry = self.model.objects.get(**self.lookup) - serialized_data = self.model_to_geo_json(entry) + entries = self.model.objects.filter(**self.lookup) + serialized_data = {} + for entry in entries: + serialized_data[str(entry.id)] = self.model_to_geo_json(entry) return serialized_data @abstractmethod diff --git a/api/utils/serializer/v1/compensation.py b/api/utils/serializer/v1/compensation.py index e2ec260..eaf1694 100644 --- a/api/utils/serializer/v1/compensation.py +++ b/api/utils/serializer/v1/compensation.py @@ -19,7 +19,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa model = Compensation def prepare_lookup(self, id, user): - self.lookup["id"] = id + super().prepare_lookup(id, user) del self.lookup["users__in"] self.lookup["intervention__users__in"] = [user] diff --git a/api/utils/serializer/v1/intervention.py b/api/utils/serializer/v1/intervention.py index 34fb75c..b77d7e3 100644 --- a/api/utils/serializer/v1/intervention.py +++ b/api/utils/serializer/v1/intervention.py @@ -90,6 +90,8 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1, Returns: obj """ + if legal_data is None: + return obj obj.legal.registration_date = legal_data["registration_date"] obj.legal.binding_date = legal_data["binding_date"] obj.legal.process_type = self.konova_code_from_json( @@ -110,6 +112,8 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1, Returns: obj """ + if responsibility_data is None: + return obj obj.responsible.registration_office = self.konova_code_from_json( responsibility_data["registration_office"], CODELIST_REGISTRATION_OFFICE_ID @@ -134,6 +138,8 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1, Returns: obj (intervention) """ + if payment_data is None: + return obj payments = [] for entry in payment_data: due_on = entry["due_on"] @@ -226,9 +232,9 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1, properties = json_model["properties"] obj.title = properties["title"] obj.modified = update_action - self.set_responsibility(obj, properties["responsible"]) - self.set_legal(obj, properties["legal"]) - self.set_payments(obj, properties["payments"]) + self.set_responsibility(obj, properties.get("responsible", None)) + self.set_legal(obj, properties.get("legal", None)) + self.set_payments(obj, properties.get("payments", None)) obj.geometry.geom = self.create_geometry_from_json(json_model) obj.geometry.modified = update_action diff --git a/api/utils/serializer/v1/serializer.py b/api/utils/serializer/v1/serializer.py index af3fa96..e97b1ee 100644 --- a/api/utils/serializer/v1/serializer.py +++ b/api/utils/serializer/v1/serializer.py @@ -50,6 +50,8 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer): Returns: serialized_json (dict) """ + if konova_code is None: + return None return { "atom_id": konova_code.atom_id, "long_name": konova_code.long_name, @@ -83,7 +85,7 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer): Returns: created_on (timestamp) """ - return entry.created.timestamp + return entry.created.timestamp if entry.created is not None else None def modified_on_to_json(self, entry): """ Serializes the modified_on into json @@ -95,7 +97,7 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer): modified_on (timestamp) """ modified_on = entry.modified or entry.created - modified_on = modified_on.timestamp + modified_on = modified_on.timestamp if modified_on is not None else None return modified_on diff --git a/api/views/v1/views.py b/api/views/v1/views.py index 7e271dd..fa55458 100644 --- a/api/views/v1/views.py +++ b/api/views/v1/views.py @@ -28,21 +28,29 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): Args: request (HttpRequest): The incoming request - id (str): The entries id + id (str): The entries id (optional) Returns: - + response (JsonResponse) """ try: - if id is None: - raise AttributeError("No id provided") self.serializer.prepare_lookup(id, self.user) data = self.serializer.fetch_and_serialize() except Exception as e: return self.return_error_response(e, 500) return JsonResponse(data) - def post(self, request: HttpRequest, id=None): + def post(self, request: HttpRequest): + """ Handles the POST request + + Performs creation of new data + + Args: + request (HttpRequest): The incoming request + + Returns: + response (JsonResponse) + """ try: body = request.body.decode("utf-8") body = json.loads(body) @@ -52,6 +60,17 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): return JsonResponse({"id": created_id}) def put(self, request: HttpRequest, id=None): + """ Handles the PUT request + + Performs updating + + Args: + request (HttpRequest): The incoming request + id (str): The entries id + + Returns: + + """ try: body = request.body.decode("utf-8") body = json.loads(body) From 89fb867ab219f79890f25c4ac7d1017e29fe32a7 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 16:43:37 +0100 Subject: [PATCH 19/36] #31 API PUT/POST Ema * adds support for PUT and POST of Ema * moves set_responsibility() and set_legal() from Intervention API Serializer into proper Mixins where they belong to --- api/utils/serializer/v1/ema.py | 105 ++++++++++++++++++++++++ api/utils/serializer/v1/intervention.py | 49 ----------- api/utils/serializer/v1/serializer.py | 50 ++++++++++- 3 files changed, 154 insertions(+), 50 deletions(-) diff --git a/api/utils/serializer/v1/ema.py b/api/utils/serializer/v1/ema.py index a2e0bc1..8b1f358 100644 --- a/api/utils/serializer/v1/ema.py +++ b/api/utils/serializer/v1/ema.py @@ -5,10 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 24.01.22 """ +from django.db import transaction + from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, \ ResponsibilityAPISerializerV1Mixin from ema.models import Ema from intervention.models import Responsibility +from konova.models import Geometry +from konova.tasks import celery_update_parcels +from user.models import UserActionLogEntry class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, ResponsibilityAPISerializerV1Mixin): @@ -27,3 +32,103 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe self.properties_data["after_states"] = self.compensation_state_to_json(entry.after_states.all()) self.properties_data["actions"] = self.compensation_actions_to_json(entry.actions.all()) self.properties_data["deadlines"] = self.deadlines_to_json(entry.deadlines.all()) + + def initialize_objects(self, json_model, user): + """ Initializes all needed objects from the json_model data + + Does not persist data to the DB! + + Args: + json_model (dict): The json data + user (User): The API user + + Returns: + obj (Compensation) + """ + create_action = UserActionLogEntry.get_created_action(user, comment="API Import") + # Create geometry + json_geom = self.create_geometry_from_json(json_model) + geometry = Geometry() + geometry.geom = json_geom + geometry.created = create_action + + # Create linked objects + obj = Ema() + obj.responsible = Responsibility() + created = create_action + obj.created = created + obj.geometry = geometry + return obj + + def create_model_from_json(self, json_model, user): + """ Creates a new entry for the model based on the contents of json_model + + Args: + json_model (dict): The json containing data + user (User): The API user + + Returns: + created_id (str): The id of the newly created Ema entry + """ + with transaction.atomic(): + obj = self.initialize_objects(json_model, user) + + # Fill in data to objects + properties = json_model["properties"] + obj.identifier = obj.generate_new_identifier() + obj.title = properties["title"] + obj = self.set_responsibility(obj, properties["responsible"]) + + obj.geometry.save() + obj.responsible.save() + obj.save() + + obj = self.set_compensation_actions(obj, properties["actions"]) + obj = self.set_compensation_states(obj, properties["before_states"], obj.before_states) + obj = self.set_compensation_states(obj, properties["after_states"], obj.after_states) + obj = self.set_deadlines(obj, properties["deadlines"]) + + obj.log.add(obj.created) + obj.users.add(user) + + celery_update_parcels.delay(obj.geometry.id) + + return obj.id + + def update_model_from_json(self, id, json_model, user): + """ Updates an entry for the model based on the contents of json_model + + Args: + id (str): The object's id + json_model (dict): The json containing data + user (User): The API user + + Returns: + created_id (str): The id of the newly created Ema entry + """ + with transaction.atomic(): + update_action = UserActionLogEntry.get_edited_action(user, "API update") + obj = self.get_obj_from_db(id, user) + + # Fill in data to objects + properties = json_model["properties"] + obj.title = properties["title"] + obj.modified = update_action + obj.geometry.geom = self.create_geometry_from_json(json_model) + obj.geometry.modified = update_action + obj = self.set_responsibility(obj, properties["responsible"]) + + obj.geometry.save() + obj.responsible.save() + obj.save() + + obj = self.set_compensation_actions(obj, properties["actions"]) + obj = self.set_compensation_states(obj, properties["before_states"], obj.before_states) + obj = self.set_compensation_states(obj, properties["after_states"], obj.after_states) + obj = self.set_deadlines(obj, properties["deadlines"]) + + obj.log.add(update_action) + + celery_update_parcels.delay(obj.geometry.id) + + return obj.id diff --git a/api/utils/serializer/v1/intervention.py b/api/utils/serializer/v1/intervention.py index b77d7e3..cca9506 100644 --- a/api/utils/serializer/v1/intervention.py +++ b/api/utils/serializer/v1/intervention.py @@ -10,8 +10,6 @@ from django.db.models import QuerySet from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, \ ResponsibilityAPISerializerV1Mixin, LegalAPISerializerV1Mixin, DeductableAPISerializerV1Mixin -from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID, \ - CODELIST_PROCESS_TYPE_ID, CODELIST_LAW_ID from compensation.models import Payment from intervention.models import Intervention, Responsibility, Legal from konova.models import Geometry @@ -80,53 +78,6 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1, obj.responsible = resp return obj - def set_legal(self, obj, legal_data): - """ Sets the legal data contents to the provided legal_data dict - - Args: - obj (Intervention): The intervention object - legal_data (dict): The new data - - Returns: - obj - """ - if legal_data is None: - return obj - obj.legal.registration_date = legal_data["registration_date"] - obj.legal.binding_date = legal_data["binding_date"] - obj.legal.process_type = self.konova_code_from_json( - legal_data["process_type"], - CODELIST_PROCESS_TYPE_ID, - ) - laws = [self.konova_code_from_json(law, CODELIST_LAW_ID) for law in legal_data["laws"]] - obj.legal.laws.set(laws) - return obj - - def set_responsibility(self, obj, responsibility_data: dict): - """ Sets the responsible data contents to the provided responsibility_data dict - - Args: - obj (Intervention): The intervention object - responsibility_data (dict): The new data - - Returns: - obj - """ - if responsibility_data is None: - return obj - obj.responsible.registration_office = self.konova_code_from_json( - responsibility_data["registration_office"], - CODELIST_REGISTRATION_OFFICE_ID - ) - obj.responsible.registration_file_number = responsibility_data["registration_file_number"] - obj.responsible.conservation_office = self.konova_code_from_json( - responsibility_data["conservation_office"], - CODELIST_CONSERVATION_OFFICE_ID, - ) - obj.responsible.conservation_file_number = responsibility_data["conservation_file_number"] - obj.responsible.handler = responsibility_data["handler"] - return obj - def set_payments(self, obj, payment_data): """ Sets the linked Payment data according to the given payment_data diff --git a/api/utils/serializer/v1/serializer.py b/api/utils/serializer/v1/serializer.py index e97b1ee..ace721e 100644 --- a/api/utils/serializer/v1/serializer.py +++ b/api/utils/serializer/v1/serializer.py @@ -12,7 +12,8 @@ from django.db.models import QuerySet from api.utils.serializer.serializer import AbstractModelAPISerializer from codelist.models import KonovaCode -from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID +from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \ + CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID from compensation.models import CompensationAction, UnitChoices, CompensationState from intervention.models import Responsibility, Legal from konova.models import Deadline, DeadlineType @@ -154,6 +155,31 @@ class ResponsibilityAPISerializerV1Mixin: "handler": responsible.handler, } + def set_responsibility(self, obj, responsibility_data: dict): + """ Sets the responsible data contents to the provided responsibility_data dict + + Args: + obj (Intervention): The intervention object + responsibility_data (dict): The new data + + Returns: + obj + """ + if responsibility_data is None: + return obj + obj.responsible.registration_office = self.konova_code_from_json( + responsibility_data.get("registration_office", None), + CODELIST_REGISTRATION_OFFICE_ID + ) + obj.responsible.registration_file_number = responsibility_data.get("registration_file_number", None) + obj.responsible.conservation_office = self.konova_code_from_json( + responsibility_data.get("conservation_office", None), + CODELIST_CONSERVATION_OFFICE_ID, + ) + obj.responsible.conservation_file_number = responsibility_data.get("conservation_file_number", None) + obj.responsible.handler = responsibility_data.get("handler", None) + return obj + class LegalAPISerializerV1Mixin: class Meta: @@ -175,6 +201,28 @@ class LegalAPISerializerV1Mixin: "laws": [self.konova_code_to_json(law) for law in legal.laws.all()], } + def set_legal(self, obj, legal_data): + """ Sets the legal data contents to the provided legal_data dict + + Args: + obj (Intervention): The intervention object + legal_data (dict): The new data + + Returns: + obj + """ + if legal_data is None: + return obj + obj.legal.registration_date = legal_data.get("registration_date", None) + obj.legal.binding_date = legal_data.get("binding_date", None) + obj.legal.process_type = self.konova_code_from_json( + legal_data.get("process_type", None), + CODELIST_PROCESS_TYPE_ID, + ) + laws = [self.konova_code_from_json(law, CODELIST_LAW_ID) for law in legal_data.get("laws", [])] + obj.legal.laws.set(laws) + return obj + class AbstractCompensationAPISerializerV1Mixin: class Meta: From 617d969a10fe26d22ae9ef1d9c928a06ef1d37f2 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Mon, 24 Jan 2022 16:56:06 +0100 Subject: [PATCH 20/36] #31 API PUT/POST EcoAccount * adds support for PUT and POST of EcoAccount API --- api/utils/serializer/v1/ecoaccount.py | 118 +++++++++++++++++++++++++- 1 file changed, 117 insertions(+), 1 deletion(-) diff --git a/api/utils/serializer/v1/ecoaccount.py b/api/utils/serializer/v1/ecoaccount.py index 5ddbffd..9fc1e63 100644 --- a/api/utils/serializer/v1/ecoaccount.py +++ b/api/utils/serializer/v1/ecoaccount.py @@ -5,10 +5,15 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 24.01.22 """ +from django.db import transaction + from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, \ LegalAPISerializerV1Mixin, ResponsibilityAPISerializerV1Mixin, DeductableAPISerializerV1Mixin from compensation.models import EcoAccount from intervention.models import Legal, Responsibility +from konova.models import Geometry +from konova.tasks import celery_update_parcels +from user.models import UserActionLogEntry class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1, @@ -39,4 +44,115 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1, "conservation_office": self.konova_code_to_json(responsible.conservation_office), "conservation_file_number": responsible.conservation_file_number, "handler": responsible.handler, - } \ No newline at end of file + } + + def set_legal(self, obj, legal_data): + obj.legal.registration_date = legal_data.get("agreement_date", None) + return obj + + def initialize_objects(self, json_model, user): + """ Initializes all needed objects from the json_model data + + Does not persist data to the DB! + + Args: + json_model (dict): The json data + user (User): The API user + + Returns: + obj (Compensation) + """ + create_action = UserActionLogEntry.get_created_action(user, comment="API Import") + # Create geometry + json_geom = self.create_geometry_from_json(json_model) + geometry = Geometry() + geometry.geom = json_geom + geometry.created = create_action + + # Create linked objects + obj = EcoAccount() + obj.responsible = Responsibility() + obj.legal = Legal() + created = create_action + obj.created = created + obj.geometry = geometry + return obj + + def create_model_from_json(self, json_model, user): + """ Creates a new entry for the model based on the contents of json_model + + Args: + json_model (dict): The json containing data + user (User): The API user + + Returns: + created_id (str): The id of the newly created EcoAccount entry + """ + with transaction.atomic(): + obj = self.initialize_objects(json_model, user) + + # Fill in data to objects + properties = json_model["properties"] + obj.identifier = obj.generate_new_identifier() + obj.title = properties["title"] + obj.deductable_surface = float(properties["deductable_surface"]) + obj = self.set_responsibility(obj, properties["responsible"]) + obj = self.set_legal(obj, properties["legal"]) + + obj.geometry.save() + obj.responsible.save() + obj.legal.save() + obj.save() + + obj = self.set_compensation_actions(obj, properties["actions"]) + obj = self.set_compensation_states(obj, properties["before_states"], obj.before_states) + obj = self.set_compensation_states(obj, properties["after_states"], obj.after_states) + obj = self.set_deadlines(obj, properties["deadlines"]) + + obj.log.add(obj.created) + obj.users.add(user) + + celery_update_parcels.delay(obj.geometry.id) + + return obj.id + + def update_model_from_json(self, id, json_model, user): + """ Updates an entry for the model based on the contents of json_model + + Args: + id (str): The object's id + json_model (dict): The json containing data + user (User): The API user + + Returns: + created_id (str): The id of the newly created EcoAccount entry + """ + with transaction.atomic(): + update_action = UserActionLogEntry.get_edited_action(user, "API update") + obj = self.get_obj_from_db(id, user) + + # Fill in data to objects + properties = json_model["properties"] + obj.title = properties["title"] + obj.deductable_surface = float(properties["deductable_surface"]) + obj.modified = update_action + obj.geometry.geom = self.create_geometry_from_json(json_model) + obj.geometry.modified = update_action + obj = self.set_responsibility(obj, properties["responsible"]) + obj = self.set_legal(obj, properties["legal"]) + + obj.geometry.save() + obj.responsible.save() + obj.legal.save() + obj.save() + + obj = self.set_compensation_actions(obj, properties["actions"]) + obj = self.set_compensation_states(obj, properties["before_states"], obj.before_states) + obj = self.set_compensation_states(obj, properties["after_states"], obj.after_states) + obj = self.set_deadlines(obj, properties["deadlines"]) + + obj.log.add(update_action) + + celery_update_parcels.delay(obj.geometry.id) + + return obj.id From 26f402fd3be420408cd203a016b6a4992253b539 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Tue, 25 Jan 2022 09:29:14 +0100 Subject: [PATCH 21/36] #31 API WIP * adds support for GET /check on intervention to run checks automatically via API --- api/urls/v1/urls.py | 2 + api/utils/serializer/v1/intervention.py | 3 +- api/views/v1/views.py | 22 +++++-- api/views/views.py | 88 +++++++++++++++++++++---- intervention/models/intervention.py | 6 +- konova/decorators.py | 12 +--- konova/models/object.py | 3 +- user/models/user.py | 31 +++++++++ 8 files changed, 132 insertions(+), 35 deletions(-) diff --git a/api/urls/v1/urls.py b/api/urls/v1/urls.py index 37cd52f..42c74c6 100644 --- a/api/urls/v1/urls.py +++ b/api/urls/v1/urls.py @@ -8,9 +8,11 @@ Created on: 21.01.22 from django.urls import path from api.views.v1.views import EmaAPIViewV1, EcoAccountAPIViewV1, CompensationAPIViewV1, InterventionAPIViewV1 +from api.views.views import InterventionCheckAPIView app_name = "v1" urlpatterns = [ + path("intervention//check", InterventionCheckAPIView.as_view(), name="intervention-check"), path("intervention/", InterventionAPIViewV1.as_view(), name="intervention"), path("intervention/", InterventionAPIViewV1.as_view(), name="intervention"), path("compensation/", CompensationAPIViewV1.as_view(), name="compensation"), diff --git a/api/utils/serializer/v1/intervention.py b/api/utils/serializer/v1/intervention.py index cca9506..a36b547 100644 --- a/api/utils/serializer/v1/intervention.py +++ b/api/utils/serializer/v1/intervention.py @@ -182,7 +182,6 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1, # Fill in data to objects properties = json_model["properties"] obj.title = properties["title"] - obj.modified = update_action self.set_responsibility(obj, properties.get("responsible", None)) self.set_legal(obj, properties.get("legal", None)) self.set_payments(obj, properties.get("payments", None)) @@ -194,7 +193,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1, obj.legal.save() obj.save() - obj.log.add(update_action) + obj.mark_as_edited(user) celery_update_parcels.delay(obj.geometry.id) diff --git a/api/views/v1/views.py b/api/views/v1/views.py index fa55458..c7ebaff 100644 --- a/api/views/v1/views.py +++ b/api/views/v1/views.py @@ -13,13 +13,23 @@ from api.utils.serializer.v1.compensation import CompensationAPISerializerV1 from api.utils.serializer.v1.ecoaccount import EcoAccountAPISerializerV1 from api.utils.serializer.v1.ema import EmaAPISerializerV1 from api.utils.serializer.v1.intervention import InterventionAPISerializerV1 -from api.views.views import AbstractModelAPIView +from api.views.views import AbstractAPIView -class AbstractModelAPIViewV1(AbstractModelAPIView): +class AbstractAPIViewV1(AbstractAPIView): """ Holds general serialization functions for API v1 """ + serializer = None + + def __init__(self, *args, **kwargs): + self.lookup = { + "id": None, # must be set in subclasses + "deleted__isnull": True, + "users__in": [], # must be set in subclasses + } + super().__init__(*args, **kwargs) + self.serializer = self.serializer() def get(self, request: HttpRequest, id=None): """ Handles the GET request @@ -80,17 +90,17 @@ class AbstractModelAPIViewV1(AbstractModelAPIView): return JsonResponse({"id": updated_id}) -class InterventionAPIViewV1(AbstractModelAPIViewV1): +class InterventionAPIViewV1(AbstractAPIViewV1): serializer = InterventionAPISerializerV1 -class CompensationAPIViewV1(AbstractModelAPIViewV1): +class CompensationAPIViewV1(AbstractAPIViewV1): serializer = CompensationAPISerializerV1 -class EcoAccountAPIViewV1(AbstractModelAPIViewV1): +class EcoAccountAPIViewV1(AbstractAPIViewV1): serializer = EcoAccountAPISerializerV1 -class EmaAPIViewV1(AbstractModelAPIViewV1): +class EmaAPIViewV1(AbstractAPIViewV1): serializer = EmaAPISerializerV1 diff --git a/api/views/views.py b/api/views/views.py index 9608aec..3d47578 100644 --- a/api/views/views.py +++ b/api/views/views.py @@ -6,15 +6,16 @@ Created on: 21.01.22 """ -from django.http import JsonResponse +from django.http import JsonResponse, HttpRequest from django.views import View from django.views.decorators.csrf import csrf_exempt from api.models import APIUserToken from api.settings import KSP_TOKEN_HEADER_IDENTIFIER +from intervention.models import Intervention -class AbstractModelAPIView(View): +class AbstractAPIView(View): """ Base class for API views The API must follow the GeoJSON Specification RFC 7946 @@ -22,21 +23,11 @@ class AbstractModelAPIView(View): https://datatracker.ietf.org/doc/html/rfc7946 """ - serializer = None user = None class Meta: abstract = True - def __init__(self, *args, **kwargs): - self.lookup = { - "id": None, # must be set in subclasses - "deleted__isnull": True, - "users__in": [], # must be set in subclasses - } - super().__init__(*args, **kwargs) - self.serializer = self.serializer() - @csrf_exempt def dispatch(self, request, *args, **kwargs): try: @@ -65,3 +56,76 @@ class AbstractModelAPIView(View): }, status=status_code ) + + +class InterventionCheckAPIView(AbstractAPIView): + + def get(self, request: HttpRequest, id): + """ Takes the GET request + + Args: + request (HttpRequest): The incoming request + id (str): The intervention's id + + Returns: + response (JsonResponse) + """ + if not self.user.is_zb_user(): + return self.return_error_response("Permission not granted", 403) + try: + obj = Intervention.objects.get( + id=id, + users__in=[self.user] + ) + except Exception as e: + return self.return_error_response(e) + + all_valid, check_details = self.run_quality_checks(obj) + + if all_valid: + log_entry = obj.set_checked(self.user) + obj.log.add(log_entry) + + data = { + "success": all_valid, + "details": check_details + } + return JsonResponse(data) + + def run_quality_checks(self, obj: Intervention) -> (bool, dict): + """ Performs a check for intervention and related compensations + + Args: + obj (Intervention): The intervention + + Returns: + all_valid (boold): Whether an error occured or not + check_details (dict): A dict containg details on which elements have errors + """ + # Run quality check for Intervention + all_valid = True + intervention_checker = obj.quality_check() + all_valid = intervention_checker.valid and all_valid + + # Run quality checks for linked compensations + comps = obj.compensations.all() + comp_checkers = [] + for comp in comps: + comp_checker = comp.quality_check() + comp_checkers.append(comp_checker) + all_valid = comp_checker.valid and all_valid + + check_details = { + "intervention": { + "id": obj.id, + "errors": intervention_checker.messages + }, + "compensations": [ + { + "id": comp_checker.obj.id, + "errors": comp_checker.messages + } + for comp_checker in comp_checkers + ] + } + return all_valid, check_details diff --git a/intervention/models/intervention.py b/intervention/models/intervention.py index 766346f..5e2aca0 100644 --- a/intervention/models/intervention.py +++ b/intervention/models/intervention.py @@ -154,6 +154,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec def set_unrecorded(self, user: User): log_entry = super().set_unrecorded(user) self.add_log_entry_to_compensations(log_entry) + return log_entry def set_recorded(self, user: User) -> UserActionLogEntry: log_entry = super().set_recorded(user) @@ -259,11 +260,6 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec Returns: """ - user_action_edit = UserActionLogEntry.get_edited_action(performing_user, comment=edit_comment) - self.log.add(user_action_edit) - self.modified = user_action_edit - self.save() - super().mark_as_edited(performing_user, request) if self.checked: self.set_unchecked() diff --git a/konova/decorators.py b/konova/decorators.py index 4edf554..583afbe 100644 --- a/konova/decorators.py +++ b/konova/decorators.py @@ -75,9 +75,7 @@ def default_group_required(function): @wraps(function) def wrap(request, *args, **kwargs): user = request.user - has_group = user.groups.filter( - name=DEFAULT_GROUP - ).exists() + has_group = user.is_default_user() if has_group: return function(request, *args, **kwargs) else: @@ -95,9 +93,7 @@ def registration_office_group_required(function): @wraps(function) def wrap(request, *args, **kwargs): user = request.user - has_group = user.groups.filter( - name=ZB_GROUP - ).exists() + has_group = user.is_zb_user() if has_group: return function(request, *args, **kwargs) else: @@ -115,9 +111,7 @@ def conservation_office_group_required(function): @wraps(function) def wrap(request, *args, **kwargs): user = request.user - has_group = user.groups.filter( - name=ETS_GROUP - ).exists() + has_group = user.is_ets_user() if has_group: return function(request, *args, **kwargs) else: diff --git a/konova/models/object.py b/konova/models/object.py index 5a7d6f1..4b3959c 100644 --- a/konova/models/object.py +++ b/konova/models/object.py @@ -277,7 +277,8 @@ class RecordableObjectMixin(models.Model): self.save() if self.recorded: - self.set_unrecorded(performing_user) + action = self.set_unrecorded(performing_user) + self.log.add(action) if request: messages.info( request, diff --git a/user/models/user.py b/user/models/user.py index c461751..540b42a 100644 --- a/user/models/user.py +++ b/user/models/user.py @@ -9,6 +9,7 @@ from django.contrib.auth.models import AbstractUser from django.db import models +from konova.settings import ZB_GROUP, DEFAULT_GROUP, ETS_GROUP from konova.utils.mailer import Mailer from user.enums import UserNotificationEnum @@ -28,6 +29,36 @@ class User(AbstractUser): id=notification_enum.value ).exists() + def is_zb_user(self): + """ Shortcut for checking whether a user is of a special group or not + + Returns: + bool + """ + return self.groups.filter( + name=ZB_GROUP + ).exists() + + def is_default_user(self): + """ Shortcut for checking whether a user is of a special group or not + + Returns: + bool + """ + return self.groups.filter( + name=DEFAULT_GROUP + ).exists() + + def is_ets_user(self): + """ Shortcut for checking whether a user is of a special group or not + + Returns: + bool + """ + return self.groups.filter( + name=ETS_GROUP + ).exists() + def send_mail_shared_access_removed(self, obj_identifier): """ Sends a mail to the user in case of removed shared access From 25cccee5d6d79ddb213c8ef8a3e96f88bdddfe02 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Wed, 26 Jan 2022 09:16:37 +0100 Subject: [PATCH 22/36] #31 API Share * adds support for GET and PUT of sharing users for all data types (compensation is shared via intervention) --- api/urls/v1/urls.py | 8 +- api/utils/serializer/v1/compensation.py | 3 +- api/views/views.py | 125 ++++++++++++++++++++++++ compensation/models/compensation.py | 23 +++++ 4 files changed, 157 insertions(+), 2 deletions(-) diff --git a/api/urls/v1/urls.py b/api/urls/v1/urls.py index 42c74c6..313a43b 100644 --- a/api/urls/v1/urls.py +++ b/api/urls/v1/urls.py @@ -8,17 +8,23 @@ Created on: 21.01.22 from django.urls import path from api.views.v1.views import EmaAPIViewV1, EcoAccountAPIViewV1, CompensationAPIViewV1, InterventionAPIViewV1 -from api.views.views import InterventionCheckAPIView +from api.views.views import InterventionCheckAPIView, InterventionAPIShareView, EcoAccountAPIShareView, EmaAPIShareView app_name = "v1" urlpatterns = [ path("intervention//check", InterventionCheckAPIView.as_view(), name="intervention-check"), + path("intervention//share", InterventionAPIShareView.as_view(), name="intervention-share"), path("intervention/", InterventionAPIViewV1.as_view(), name="intervention"), path("intervention/", InterventionAPIViewV1.as_view(), name="intervention"), + path("compensation/", CompensationAPIViewV1.as_view(), name="compensation"), path("compensation/", CompensationAPIViewV1.as_view(), name="compensation"), + + path("ecoaccount//share", EcoAccountAPIShareView.as_view(), name="ecoaccount-share"), path("ecoaccount/", EcoAccountAPIViewV1.as_view(), name="ecoaccount"), path("ecoaccount/", EcoAccountAPIViewV1.as_view(), name="ecoaccount"), + + path("ema//share", EmaAPIShareView.as_view(), name="ema-share"), path("ema/", EmaAPIViewV1.as_view(), name="ema"), path("ema/", EmaAPIViewV1.as_view(), name="ema"), ] diff --git a/api/utils/serializer/v1/compensation.py b/api/utils/serializer/v1/compensation.py index eaf1694..cc3428b 100644 --- a/api/utils/serializer/v1/compensation.py +++ b/api/utils/serializer/v1/compensation.py @@ -12,6 +12,7 @@ from compensation.models import Compensation from intervention.models import Intervention from konova.models import Geometry from konova.tasks import celery_update_parcels +from konova.utils.message_templates import DATA_UNSHARED from user.models import UserActionLogEntry @@ -88,7 +89,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa is_shared = intervention.is_shared_with(user) if not is_shared: - raise PermissionError("Intervention not shared with user") + raise PermissionError(DATA_UNSHARED) obj.intervention = intervention return obj diff --git a/api/views/views.py b/api/views/views.py index 3d47578..32b0e50 100644 --- a/api/views/views.py +++ b/api/views/views.py @@ -5,14 +5,20 @@ Contact: michel.peltriaux@sgdnord.rlp.de Created on: 21.01.22 """ +import json +from django.db.models import QuerySet from django.http import JsonResponse, HttpRequest from django.views import View from django.views.decorators.csrf import csrf_exempt from api.models import APIUserToken from api.settings import KSP_TOKEN_HEADER_IDENTIFIER +from compensation.models import EcoAccount +from ema.models import Ema from intervention.models import Intervention +from konova.utils.message_templates import DATA_UNSHARED +from user.models import User class AbstractAPIView(View): @@ -129,3 +135,122 @@ class InterventionCheckAPIView(AbstractAPIView): ] } return all_valid, check_details + + +class AbstractModelShareAPIView(AbstractAPIView): + model = None + + class Meta: + abstract = True + + def get(self, request: HttpRequest, id): + """ Performs the GET request handling + + Args: + request (HttpRequest): The incoming request + id (str): The object's id + + Returns: + + """ + try: + users = self._get_shared_users_of_object(id) + except Exception as e: + return self.return_error_response(e) + + data = { + "users": [ + user.username for user in users + ] + } + + return JsonResponse(data) + + def put(self, request: HttpRequest, id): + """ Performs the PUT request handling + + Args: + request (HttpRequest): The incoming request + id (str): The object's id + + Returns: + + """ + + try: + success = self._process_put_body(request.body, id) + except Exception as e: + return self.return_error_response(e) + data = { + "success": success, + } + return JsonResponse(data) + + def _check_user_has_shared_access(self, obj): + """ Raises a PermissionError if user has no shared access + + Args: + obj (BaseObject): The object + + Returns: + + """ + is_shared = obj.is_shared_with(self.user) + if not is_shared: + raise PermissionError(DATA_UNSHARED) + + def _get_shared_users_of_object(self, id) -> QuerySet: + """ Check permissions and get the users + + Args: + id (str): The object's id + + Returns: + users (QuerySet) + """ + obj = self.model.objects.get( + id=id + ) + self._check_user_has_shared_access(obj) + users = obj.shared_users + return users + + def _process_put_body(self, body: bytes, id: str): + """ Reads the body data, performs validity checks and sets the new users + + Args: + body (bytes): The request.body + id (str): The object's id + + Returns: + success (bool) + """ + obj = self.model.objects.get(id=id) + self._check_user_has_shared_access(obj) + + new_users = json.loads(body.decode("utf-8")) + new_users = new_users.get("users", []) + if len(new_users) == 0: + raise ValueError("Shared user list must not be empty!") + + # Eliminate duplicates + new_users = list(dict.fromkeys(new_users)) + + # Make sure each of these names exist as a user + new_users_objs = [] + for user in new_users: + new_users_objs.append(User.objects.get(username=user)) + obj.share_with_list(new_users_objs) + return True + + +class InterventionAPIShareView(AbstractModelShareAPIView): + model = Intervention + + +class EcoAccountAPIShareView(AbstractModelShareAPIView): + model = EcoAccount + + +class EmaAPIShareView(AbstractModelShareAPIView): + model = Ema \ No newline at end of file diff --git a/compensation/models/compensation.py b/compensation/models/compensation.py index 59a3fd9..89d6dc2 100644 --- a/compensation/models/compensation.py +++ b/compensation/models/compensation.py @@ -245,6 +245,29 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin): # Compensations inherit their shared state from the interventions return self.intervention.is_shared_with(user) + def share_with(self, user: User): + """ Adds user to list of shared access users + + Args: + user (User): The user to be added to the object + + Returns: + + """ + if not self.intervention.is_shared_with(user): + self.intervention.users.add(user) + + def share_with_list(self, user_list: list): + """ Sets the list of shared access users + + Args: + user_list (list): The users to be added to the object + + Returns: + + """ + self.intervention.users.set(user_list) + @property def shared_users(self) -> QuerySet: """ Shortcut for fetching the users which have shared access on this object From b13e67e061322f06ee55239d04e64c5b11d6a755 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Thu, 27 Jan 2022 11:37:38 +0100 Subject: [PATCH 23/36] #31 API Frontend token generating * adds frontend settings for users to create API tokens on their user settings --- api/urls/urls.py | 3 + api/views/method_views.py | 35 +++++++ api/views/views.py | 2 +- compensation/views/compensation.py | 2 +- compensation/views/eco_account.py | 2 +- ema/views.py | 2 +- intervention/views.py | 2 +- .../widgets/generate-content-input.html | 6 +- locale/de/LC_MESSAGES/django.mo | Bin 34703 -> 35548 bytes locale/de/LC_MESSAGES/django.po | 89 ++++++++++++++---- user/forms.py | 48 +++++++++- user/models/user.py | 17 ++++ user/templates/user/index.html | 8 ++ user/templates/user/token.html | 31 ++++++ user/urls.py | 1 + user/views.py | 36 ++++++- 16 files changed, 255 insertions(+), 29 deletions(-) create mode 100644 api/views/method_views.py create mode 100644 user/templates/user/token.html diff --git a/api/urls/urls.py b/api/urls/urls.py index fe0ddbb..abe9eac 100644 --- a/api/urls/urls.py +++ b/api/urls/urls.py @@ -7,8 +7,11 @@ Created on: 21.01.22 """ from django.urls import path, include +from api.views.method_views import generate_new_token_view + app_name = "api" urlpatterns = [ path("v1/", include("api.urls.v1.urls", namespace="v1")), + path("token/generate", generate_new_token_view, name="generate-new-token"), ] \ No newline at end of file diff --git a/api/views/method_views.py b/api/views/method_views.py new file mode 100644 index 0000000..7c6904a --- /dev/null +++ b/api/views/method_views.py @@ -0,0 +1,35 @@ +""" +Author: Michel Peltriaux +Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany +Contact: michel.peltriaux@sgdnord.rlp.de +Created on: 27.01.22 + +""" +from django.contrib.auth.decorators import login_required +from django.http import HttpRequest, JsonResponse + +from api.models import APIUserToken + + +@login_required +def generate_new_token_view(request: HttpRequest): + """ Handles request for fetching + + Args: + request (HttpRequest): The incoming request + + Returns: + + """ + + if request.method == "GET": + token = APIUserToken() + while APIUserToken.objects.filter(token=token.token).exists(): + token = APIUserToken() + return JsonResponse( + data={ + "gen_data": token.token + } + ) + else: + raise NotImplementedError \ No newline at end of file diff --git a/api/views/views.py b/api/views/views.py index 32b0e50..8cbdc1e 100644 --- a/api/views/views.py +++ b/api/views/views.py @@ -253,4 +253,4 @@ class EcoAccountAPIShareView(AbstractModelShareAPIView): class EmaAPIShareView(AbstractModelShareAPIView): - model = Ema \ No newline at end of file + model = Ema diff --git a/compensation/views/compensation.py b/compensation/views/compensation.py index 323921e..009eaf8 100644 --- a/compensation/views/compensation.py +++ b/compensation/views/compensation.py @@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest): identifier = tmp.generate_new_identifier() return JsonResponse( data={ - "identifier": identifier + "gen_data": identifier } ) diff --git a/compensation/views/eco_account.py b/compensation/views/eco_account.py index 5c061b9..59b82a7 100644 --- a/compensation/views/eco_account.py +++ b/compensation/views/eco_account.py @@ -118,7 +118,7 @@ def new_id_view(request: HttpRequest): identifier = tmp.generate_new_identifier() return JsonResponse( data={ - "identifier": identifier + "gen_data": identifier } ) diff --git a/ema/views.py b/ema/views.py index 1e8b089..8a3454e 100644 --- a/ema/views.py +++ b/ema/views.py @@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest): identifier = tmp.generate_new_identifier() return JsonResponse( data={ - "identifier": identifier + "gen_data": identifier } ) diff --git a/intervention/views.py b/intervention/views.py index 50856df..3d65b7d 100644 --- a/intervention/views.py +++ b/intervention/views.py @@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest): identifier = tmp_intervention.generate_new_identifier() return JsonResponse( data={ - "identifier": identifier + "gen_data": identifier } ) diff --git a/konova/templates/konova/widgets/generate-content-input.html b/konova/templates/konova/widgets/generate-content-input.html index 66c9198..9d2f983 100644 --- a/konova/templates/konova/widgets/generate-content-input.html +++ b/konova/templates/konova/widgets/generate-content-input.html @@ -1,9 +1,9 @@ {% load i18n fontawesome_5 %}
- +
- {% fa5_icon 'dice' %} + {% fa5_icon 'dice' %}