From 134651c8f70c3ec042e66e02c12b90ba14487b45 Mon Sep 17 00:00:00 2001 From: mpeltriaux Date: Fri, 21 Jan 2022 15:26:08 +0100 Subject: [PATCH] #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 00000000..e69de29b diff --git a/api/admin.py b/api/admin.py new file mode 100644 index 00000000..c5e1fbac --- /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 00000000..d87006dd --- /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 00000000..d43f0c4e --- /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 00000000..f52cf7c8 --- /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 00000000..d49ff473 --- /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 00000000..7ce503c2 --- /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 00000000..14cc6f5a --- /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 00000000..62aa73d3 --- /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 00000000..c6636d5b --- /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 00000000..ff02623c --- /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 00000000..8d87c347 --- /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 00000000..d49ff473 --- /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 00000000..10f801a3 --- /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 00000000..6085d0d5 --- /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 00000000..e04027b4 --- /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 bc1a7511..052332f9 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 f4de5eca..642b1445 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 e8cc9d2d..78e075ad 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 837f0c6c..c4617510 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(