Merge branch 'master' into Docker

# Conflicts:
#	konova/sub_settings/django_settings.py
pull/109/head
mpeltriaux 3 years ago
commit 5b7351e331

@ -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)

@ -0,0 +1,5 @@
from django.apps import AppConfig
class ApiConfig(AppConfig):
name = 'api'

@ -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 *

@ -0,0 +1,50 @@
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
from django.utils import timezone
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
@staticmethod
def get_user_from_token(token: str, username: str):
""" Getter for the related user object
Args:
token (str): The used token
username (str): The username
Returns:
user (User): Otherwise None
"""
_today = timezone.now().date()
try:
token_obj = APIUserToken.objects.get(
token=token,
user__username=username
)
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("Credentials invalid")
return token_obj.user

@ -0,0 +1,9 @@
"""
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"
KSP_USER_HEADER_IDENTIFIER = "Kspuser"

@ -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
"""

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 27.01.22
"""

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 27.01.22
"""

@ -0,0 +1,19 @@
{
"type": "MultiPolygon",
"coordinates": [
],
"properties": {
"title": "Test_compensation",
"is_cef": false,
"is_coherence_keeping": false,
"intervention": "MUST_BE_SET_IN_TEST",
"before_states": [
],
"after_states": [
],
"actions": [
],
"deadlines": [
]
}
}

@ -0,0 +1,5 @@
{
"eco_account": "CHANGE_BEFORE_RUN!!!",
"surface": 500.0,
"intervention": "CHANGE_BEFORE_RUN!!!"
}

@ -0,0 +1,25 @@
{
"type": "MultiPolygon",
"coordinates": [
],
"properties": {
"title": "Test_ecoaccount",
"deductable_surface": 10000.0,
"responsible": {
"conservation_office": null,
"conservation_file_number": null,
"handler": null
},
"legal": {
"agreement_date": null
},
"before_states": [
],
"after_states": [
],
"actions": [
],
"deadlines": [
]
}
}

@ -0,0 +1,21 @@
{
"type": "MultiPolygon",
"coordinates": [
],
"properties": {
"title": "Test_ema",
"responsible": {
"conservation_office": null,
"conservation_file_number": null,
"handler": null
},
"before_states": [
],
"after_states": [
],
"actions": [
],
"deadlines": [
]
}
}

@ -0,0 +1,21 @@
{
"type": "MultiPolygon",
"coordinates": [
],
"properties": {
"title": "Test_intervention",
"responsible": {
"registration_office": null,
"registration_file_number": null,
"conservation_office": null,
"conservation_file_number": null,
"handler": null
},
"legal": {
"registration_date": null,
"binding_date": null,
"process_type": null,
"laws": []
}
}
}

@ -0,0 +1,122 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 27.01.22
"""
import json
from django.urls import reverse
from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase
class APIV1CreateTestCase(BaseAPIV1TestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
def _run_create_request(self, url, data):
data = json.dumps(data)
response = self.client.post(
url,
data=data,
content_type="application/json",
**self.header_data
)
return response
def _test_create_object(self, url, post_body):
""" Tests the API creation of a new data object.
Post body data stored in a local json file
Args:
url (str): The api creation url
post_body (dict): The post body content as dict
Returns:
"""
response = self._run_create_request(url, post_body)
self.assertEqual(response.status_code, 200, msg=response.content)
content = json.loads(response.content)
self.assertIsNotNone(content.get("id", None), msg=response.content)
def test_create_intervention(self):
""" Tests api creation
Returns:
"""
url = reverse("api:v1:intervention")
json_file_path = "api/tests/v1/create/intervention_create_post_body.json"
with open(json_file_path) as json_file:
post_body = json.load(fp=json_file)
self._test_create_object(url, post_body)
def test_create_compensation(self):
""" Tests api creation
Returns:
"""
url = reverse("api:v1:compensation")
json_file_path = "api/tests/v1/create/compensation_create_post_body.json"
with open(json_file_path) as json_file:
post_body = json.load(fp=json_file)
post_body["properties"]["intervention"] = str(self.intervention.id)
# Expect this first request to fail, since user has no shared access on the intervention, we want to create
# a compensation for
response = self._run_create_request(url, post_body)
self.assertEqual(response.status_code, 500, msg=response.content)
content = json.loads(response.content)
self.assertGreater(len(content.get("errors", [])), 0, msg=response.content)
# Add the user to the shared users of the intervention and try again! Now everything should work as expected.
self.intervention.users.add(self.superuser)
self._test_create_object(url, post_body)
def test_create_eco_account(self):
""" Tests api creation
Returns:
"""
url = reverse("api:v1:ecoaccount")
json_file_path = "api/tests/v1/create/ecoaccount_create_post_body.json"
with open(json_file_path) as json_file:
post_body = json.load(fp=json_file)
self._test_create_object(url, post_body)
def test_create_ema(self):
""" Tests api creation
Returns:
"""
url = reverse("api:v1:ema")
json_file_path = "api/tests/v1/create/ema_create_post_body.json"
with open(json_file_path) as json_file:
post_body = json.load(fp=json_file)
self._test_create_object(url, post_body)
def test_create_deduction(self):
""" Tests api creation
Returns:
"""
self.intervention.share_with(self.superuser)
self.eco_account.share_with(self.superuser)
url = reverse("api:v1:deduction")
json_file_path = "api/tests/v1/create/deduction_create_post_body.json"
with open(json_file_path) as json_file:
post_body = json.load(fp=json_file)
post_body["intervention"] = str(self.intervention.id)
post_body["eco_account"] = str(self.eco_account.id)
self._test_create_object(url, post_body)

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 28.01.22
"""

@ -0,0 +1,118 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 28.01.22
"""
import json
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase
class APIV1DeleteTestCase(BaseAPIV1TestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
def _run_delete_request(self, url):
response = self.client.delete(
url,
**self.header_data
)
return response
def _test_delete_object(self, obj, url):
""" Tests the API DELETE of a data object.
Args:
url (str): The api delete url
Returns:
"""
obj.refresh_from_db()
_id = obj.id
self.assertIsNotNone(_id)
self.assertIsNone(obj.deleted)
response = self._run_delete_request(url)
content = json.loads(response.content)
self.assertEqual(response.status_code, 200, msg=response.content)
self.assertTrue(content.get("success", False), msg=response.content)
obj.refresh_from_db()
self.assertIsNotNone(obj.deleted)
self.assertEqual(obj.deleted.user, self.superuser)
def test_delete_intervention(self):
""" Tests api creation of bare minimum interventions
Returns:
"""
test_intervention = self.create_dummy_intervention()
test_intervention.share_with(self.superuser)
url = reverse("api:v1:intervention", args=(str(test_intervention.id),))
self._test_delete_object(test_intervention, url)
def test_delete_compensation(self):
""" Tests api creation of bare minimum interventions
Returns:
"""
test_comp = self.create_dummy_compensation()
test_comp.share_with(self.superuser)
url = reverse("api:v1:compensation", args=(str(test_comp.id),))
self._test_delete_object(test_comp, url)
def test_delete_eco_account(self):
""" Tests api creation of bare minimum interventions
Returns:
"""
test_acc = self.create_dummy_eco_account()
test_acc.share_with(self.superuser)
url = reverse("api:v1:ecoaccount", args=(str(test_acc.id),))
self._test_delete_object(test_acc, url)
def test_delete_ema(self):
""" Tests api creation of bare minimum interventions
Returns:
"""
test_ema = self.create_dummy_ema()
test_ema.share_with(self.superuser)
url = reverse("api:v1:ema", args=(str(test_ema.id),))
self._test_delete_object(test_ema, url)
def test_delete_deduction(self):
""" Tests api creation of bare minimum interventions
Returns:
"""
test_deduction = self.create_dummy_deduction()
test_deduction.intervention.share_with(self.superuser)
url = reverse("api:v1:deduction", args=(str(test_deduction.id),))
response = self._run_delete_request(url)
content = json.loads(response.content)
self.assertEqual(response.status_code, 200, msg=response.content)
self.assertTrue(content.get("success", False), msg=response.content)
try:
test_deduction.refresh_from_db()
self.fail("Deduction is not deleted from db!")
except ObjectDoesNotExist:
pass

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 28.01.22
"""

@ -0,0 +1,187 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 28.01.22
"""
import json
from django.urls import reverse
from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase
class APIV1GetTestCase(BaseAPIV1TestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
def _run_get_request(self, url):
response = self.client.get(
url,
**self.header_data
)
return response
def _test_get_object(self, obj, url):
""" Tests the API GET of a data object.
Args:
url (str): The api get url
Returns:
"""
response = self._run_get_request(url)
content = json.loads(response.content)
geojson = content[str(obj.id)]
self.assertEqual(response.status_code, 200, msg=response.content)
return geojson
def _assert_geojson_format(self, geojson):
try:
geojson["type"]
geojson["coordinates"]
props = geojson["properties"]
props["id"]
props["identifier"]
props["title"]
props["created_on"]
props["modified_on"]
except KeyError as e:
self.fail(e)
def test_get_intervention(self):
""" Tests api GET
Returns:
"""
self.intervention.share_with(self.superuser)
url = reverse("api:v1:intervention", args=(str(self.intervention.id),))
geojson = self._test_get_object(self.intervention, url)
self._assert_geojson_format(geojson)
try:
props = geojson["properties"]
props["responsible"]
props["responsible"]["registration_office"]
props["responsible"]["registration_file_number"]
props["responsible"]["conservation_office"]
props["responsible"]["conservation_file_number"]
props["legal"]["registration_date"]
props["legal"]["binding_date"]
props["legal"]["process_type"]
props["legal"]["laws"]
props["compensations"]
props["payments"]
props["deductions"]
except KeyError as e:
self.fail(e)
def test_get_compensation(self):
""" Tests api GET
Returns:
"""
self.intervention.share_with(self.superuser)
self.compensation.intervention = self.intervention
self.compensation.save()
url = reverse("api:v1:compensation", args=(str(self.compensation.id),))
geojson = self._test_get_object(self.compensation, url)
self._assert_geojson_format(geojson)
try:
props = geojson["properties"]
props["is_cef"]
props["is_coherence_keeping"]
props["intervention"]
props["intervention"]["id"]
props["intervention"]["identifier"]
props["intervention"]["title"]
props["before_states"]
props["after_states"]
props["actions"]
props["deadlines"]
except KeyError as e:
self.fail(e)
def test_get_eco_account(self):
""" Tests api GET
Returns:
"""
self.eco_account.share_with(self.superuser)
url = reverse("api:v1:ecoaccount", args=(str(self.eco_account.id),))
geojson = self._test_get_object(self.eco_account, url)
self._assert_geojson_format(geojson)
try:
props = geojson["properties"]
props["deductable_surface"]
props["deductable_surface_available"]
props["responsible"]
props["responsible"]["conservation_office"]
props["responsible"]["conservation_file_number"]
props["responsible"]["handler"]
props["legal"]
props["legal"]["agreement_date"]
props["before_states"]
props["after_states"]
props["actions"]
props["deadlines"]
props["deductions"]
except KeyError as e:
self.fail(e)
def test_get_ema(self):
""" Tests api GET
Returns:
"""
self.ema.share_with(self.superuser)
url = reverse("api:v1:ema", args=(str(self.ema.id),))
geojson = self._test_get_object(self.ema, url)
self._assert_geojson_format(geojson)
try:
props = geojson["properties"]
props["responsible"]
props["responsible"]["conservation_office"]
props["responsible"]["conservation_file_number"]
props["responsible"]["handler"]
props["before_states"]
props["after_states"]
props["actions"]
props["deadlines"]
except KeyError as e:
self.fail(e)
def test_get_deduction(self):
""" Tests api GET
Returns:
"""
self.deduction.intervention.share_with(self.superuser)
url = reverse("api:v1:deduction", args=(str(self.deduction.id),))
_json = self._test_get_object(self.deduction, url)
try:
_json["id"]
_json["eco_account"]
_json["eco_account"]["id"]
_json["eco_account"]["identifier"]
_json["eco_account"]["title"]
_json["surface"]
_json["intervention"]
_json["intervention"]["id"]
_json["intervention"]["identifier"]
_json["intervention"]["title"]
except KeyError as e:
self.fail(e)

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 28.01.22
"""

@ -0,0 +1,153 @@
import json
from django.urls import reverse
from konova.settings import DEFAULT_GROUP
from konova.tests.test_views import BaseTestCase
from konova.utils.user_checks import is_default_group_only
class BaseAPIV1TestCase(BaseTestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
cls.superuser.get_API_token()
cls.superuser.api_token.is_active = True
cls.superuser.api_token.save()
default_group = cls.groups.get(name=DEFAULT_GROUP)
cls.superuser.groups.add(default_group)
cls.header_data = {
"HTTP_ksptoken": cls.superuser.api_token.token,
"HTTP_kspuser": cls.superuser.username,
}
class APIV1SharingTestCase(BaseAPIV1TestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
def _run_share_request(self, url, user_list: list):
data = {
"users": user_list
}
data = json.dumps(data)
response = self.client.put(
url,
data,
**self.header_data
)
return response
def _test_api_sharing(self, obj, url):
""" Generic test for testing sharing of a ShareableObjectMixin object
Args:
obj (ShareableObjectMixin): The object
url (str): The url to be used for a request
Returns:
"""
self.assertEqual(obj.users.count(), 0)
user_list = [
self.superuser.username,
self.user.username,
]
response = self._run_share_request(url, user_list)
# Must fail, since performing user has no access on requested object
self.assertEqual(response.status_code, 500)
self.assertTrue(len(json.loads(response.content.decode("utf-8")).get("errors", [])) > 0)
# Add performing user to shared access users and rerun the request
obj.users.add(self.superuser)
response = self._run_share_request(url, user_list)
shared_users = obj.shared_users
self.assertEqual(response.status_code, 200)
self.assertEqual(shared_users.count(), 2)
self.assertIn(self.superuser, shared_users)
self.assertIn(self.user, shared_users)
def test_api_token_invalid(self):
""" Tests that a request with an invalid token won't be successfull
Returns:
"""
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
# Expect the first request to work properly
self.intervention.users.add(self.superuser)
response = self._run_share_request(share_url, [self.superuser.username])
self.assertEqual(response.status_code, 200)
# Change the token
self.header_data["HTTP_ksptoken"] = f"{self.superuser.api_token.token}__X"
# Expect the request to fail now
response = self._run_share_request(share_url, [self.superuser.username])
self.assertEqual(response.status_code, 403)
def test_api_intervention_sharing(self):
""" Tests proper sharing of intervention
Returns:
"""
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
self._test_api_sharing(self.intervention, share_url)
def test_api_eco_account_sharing(self):
""" Tests proper sharing of eco account
Returns:
"""
share_url = reverse("api:v1:ecoaccount-share", args=(self.eco_account.id,))
self._test_api_sharing(self.eco_account, share_url)
def test_api_ema_sharing(self):
""" Tests proper sharing of ema
Returns:
"""
share_url = reverse("api:v1:ema-share", args=(self.ema.id,))
self._test_api_sharing(self.ema, share_url)
def test_api_sharing_as_default_group_only(self):
""" Tests that sharing using the API as an only default group user works as expected.
Expected:
Default only user can only add new users, having shared access. Removing them from the list of users
having shared access is only possible if the user has further rights, e.g. being part of a registration
or conservation office group.
Returns:
"""
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
# Give the user only default group rights
default_group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([default_group])
self.assertTrue(is_default_group_only(self.superuser))
# Add only him as shared_users an object
self.intervention.users.set([self.superuser])
self.assertEqual(self.intervention.users.count(), 1)
# Try to add another user via API -> must work!
response = self._run_share_request(share_url, [self.superuser.username, self.user.username])
self.assertEqual(response.status_code, 200)
self.assertEqual(self.intervention.users.count(), 2)
# Now try to remove the user again -> expect no changes at all to the shared user list
response = self._run_share_request(share_url, [self.superuser.username])
self.assertEqual(response.status_code, 200)
self.assertEqual(self.intervention.users.count(), 2)

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 28.01.22
"""

@ -0,0 +1,61 @@
{
"type": "MultiPolygon",
"coordinates": [
[
[
[
7.845568656921382,
50.79829702304368
],
[
7.837371826171871,
50.80155187891526
],
[
7.835698127746578,
50.805267562209806
],
[
7.841062545776364,
50.806623577403386
],
[
7.848916053771969,
50.808359219420474
],
[
7.855696678161618,
50.807057493952975
],
[
7.854666709899899,
50.80423696434001
],
[
7.850461006164548,
50.80217570040005
],
[
7.845568656921382,
50.79829702304368
]
]
]
],
"properties": {
"title": "TEST_compensation_CHANGED",
"is_cef": true,
"is_coherence_keeping": true,
"intervention": "CHANGE_BEFORE_RUN!!!",
"before_states": [],
"after_states": [],
"actions": [],
"deadlines": [
{
"type": "finished",
"date": "2022-01-31",
"comment": "TEST_CHANGED"
}
]
}
}

@ -0,0 +1,5 @@
{
"eco_account": "CHANGE_BEFORE_RUN!!!",
"surface": 523400.0,
"intervention": "CHANGE_BEFORE_RUN!!!"
}

@ -0,0 +1,70 @@
{
"type": "MultiPolygon",
"coordinates": [
[
[
[
7.845568656921382,
50.79829702304368
],
[
7.837371826171871,
50.80155187891526
],
[
7.835698127746578,
50.805267562209806
],
[
7.841062545776364,
50.806623577403386
],
[
7.848916053771969,
50.808359219420474
],
[
7.855696678161618,
50.807057493952975
],
[
7.854666709899899,
50.80423696434001
],
[
7.850461006164548,
50.80217570040005
],
[
7.845568656921382,
50.79829702304368
]
]
]
],
"properties": {
"title": "TEST_account_CHANGED",
"deductable_surface": "100000.0",
"responsible": {
"conservation_office": null,
"conservation_file_number": "123-TEST",
"handler": "TEST_HANDLER_CHANGED"
},
"legal": {
"agreement_date": "2022-01-11"
},
"before_states": [
],
"after_states": [
],
"actions": [
],
"deadlines": [
{
"type": "finished",
"date": "2022-01-31",
"comment": "TEST_CHANGED"
}
]
}
}

@ -0,0 +1,63 @@
{
"type": "MultiPolygon",
"coordinates": [
[
[
[
7.845568656921382,
50.79829702304368
],
[
7.837371826171871,
50.80155187891526
],
[
7.835698127746578,
50.805267562209806
],
[
7.841062545776364,
50.806623577403386
],
[
7.848916053771969,
50.808359219420474
],
[
7.855696678161618,
50.807057493952975
],
[
7.854666709899899,
50.80423696434001
],
[
7.850461006164548,
50.80217570040005
],
[
7.845568656921382,
50.79829702304368
]
]
]
],
"properties": {
"title": "TEST_EMA_CHANGED",
"responsible": {
"conservation_office": null,
"conservation_file_number": "TEST_CHANGED",
"handler": "TEST_HANDLER_CHANGED"
},
"before_states": [],
"after_states": [],
"actions": [],
"deadlines": [
{
"type": "finished",
"date": "2022-01-31",
"comment": "TEST_CHANGED"
}
]
}
}

@ -0,0 +1,61 @@
{
"type": "MultiPolygon",
"coordinates": [
[
[
[
7.845568656921382,
50.79829702304368
],
[
7.837371826171871,
50.80155187891526
],
[
7.835698127746578,
50.805267562209806
],
[
7.841062545776364,
50.806623577403386
],
[
7.848916053771969,
50.808359219420474
],
[
7.855696678161618,
50.807057493952975
],
[
7.854666709899899,
50.80423696434001
],
[
7.850461006164548,
50.80217570040005
],
[
7.845568656921382,
50.79829702304368
]
]
]
],
"properties": {
"title": "Test_intervention_CHANGED",
"responsible": {
"registration_office": null,
"registration_file_number": "CHANGED",
"conservation_office": null,
"conservation_file_number": "CHANGED",
"handler": null
},
"legal": {
"registration_date": "2022-02-01",
"binding_date": "2022-02-01",
"process_type": null,
"laws": []
}
}
}

@ -0,0 +1,186 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 28.01.22
"""
import json
from django.contrib.gis import geos
from django.urls import reverse
from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase
class APIV1UpdateTestCase(BaseAPIV1TestCase):
@classmethod
def setUpTestData(cls):
super().setUpTestData()
def _run_update_request(self, url, data):
data = json.dumps(data)
response = self.client.put(
url,
data=data,
content_type="application/json",
**self.header_data
)
return response
def _test_update_object(self, url, put_body):
""" Tests the API update of a data object.
Put body data stored in a local json file
Args:
url (str): The api creation url
put_body (dict): The put body content as dict
Returns:
"""
response = self._run_update_request(url, put_body)
self.assertEqual(response.status_code, 200, msg=response.content)
content = json.loads(response.content)
self.assertIsNotNone(content.get("id", None), msg=response.content)
def test_update_intervention(self):
""" Tests api update
Returns:
"""
self.intervention.share_with(self.superuser)
modified_on = self.intervention.modified
url = reverse("api:v1:intervention", args=(str(self.intervention.id),))
json_file_path = "api/tests/v1/update/intervention_update_put_body.json"
with open(json_file_path) as json_file:
put_body = json.load(fp=json_file)
self._test_update_object(url, put_body)
self.intervention.refresh_from_db()
put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body))
self.assertEqual(put_geom, self.intervention.geometry.geom)
self.assertEqual(put_props["title"], self.intervention.title)
self.assertNotEqual(modified_on, self.intervention.modified)
self.assertEqual(put_props["responsible"]["registration_file_number"], self.intervention.responsible.registration_file_number)
self.assertEqual(put_props["responsible"]["conservation_file_number"], self.intervention.responsible.conservation_file_number)
self.assertEqual(put_props["legal"]["registration_date"], str(self.intervention.legal.registration_date))
self.assertEqual(put_props["legal"]["binding_date"], str(self.intervention.legal.binding_date))
def test_update_compensation(self):
""" Tests api update
Returns:
"""
self.compensation.intervention = self.intervention
self.compensation.save()
self.intervention.share_with(self.superuser)
modified_on = self.compensation.modified
url = reverse("api:v1:compensation", args=(str(self.compensation.id),))
json_file_path = "api/tests/v1/update/compensation_update_put_body.json"
with open(json_file_path) as json_file:
put_body = json.load(fp=json_file)
put_body["properties"]["intervention"] = str(self.intervention.id)
self._test_update_object(url, put_body)
self.compensation.refresh_from_db()
put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body))
self.assertEqual(put_geom, self.compensation.geometry.geom)
self.assertEqual(put_props["title"], self.compensation.title)
self.assertNotEqual(modified_on, self.compensation.modified)
self.assertEqual(put_props["is_cef"], self.compensation.is_cef)
self.assertEqual(put_props["is_coherence_keeping"], self.compensation.is_coherence_keeping)
self.assertEqual(len(put_props["actions"]), self.compensation.actions.count())
self.assertEqual(len(put_props["before_states"]), self.compensation.before_states.count())
self.assertEqual(len(put_props["after_states"]), self.compensation.after_states.count())
self.assertEqual(len(put_props["deadlines"]), self.compensation.deadlines.count())
def test_update_ecoaccount(self):
""" Tests api update
Returns:
"""
self.eco_account.share_with(self.superuser)
modified_on = self.eco_account.modified
url = reverse("api:v1:ecoaccount", args=(str(self.eco_account.id),))
json_file_path = "api/tests/v1/update/ecoaccount_update_put_body.json"
with open(json_file_path) as json_file:
put_body = json.load(fp=json_file)
self._test_update_object(url, put_body)
self.eco_account.refresh_from_db()
put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body))
self.assertEqual(put_geom, self.eco_account.geometry.geom)
self.assertEqual(put_props["title"], self.eco_account.title)
self.assertNotEqual(modified_on, self.eco_account.modified)
self.assertEqual(put_props["deductable_surface"], str(self.eco_account.deductable_surface))
self.assertEqual(put_props["responsible"]["conservation_office"], self.eco_account.responsible.conservation_office)
self.assertEqual(put_props["responsible"]["conservation_file_number"], self.eco_account.responsible.conservation_file_number)
self.assertEqual(put_props["responsible"]["handler"], self.eco_account.responsible.handler)
self.assertEqual(put_props["legal"]["agreement_date"], str(self.eco_account.legal.registration_date))
self.assertEqual(len(put_props["actions"]), self.eco_account.actions.count())
self.assertEqual(len(put_props["before_states"]), self.eco_account.before_states.count())
self.assertEqual(len(put_props["after_states"]), self.eco_account.after_states.count())
self.assertEqual(len(put_props["deadlines"]), self.eco_account.deadlines.count())
def test_update_ema(self):
""" Tests api update
Returns:
"""
self.ema.share_with(self.superuser)
modified_on = self.ema.modified
url = reverse("api:v1:ema", args=(str(self.ema.id),))
json_file_path = "api/tests/v1/update/ema_update_put_body.json"
with open(json_file_path) as json_file:
put_body = json.load(fp=json_file)
self._test_update_object(url, put_body)
self.ema.refresh_from_db()
put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body))
self.assertEqual(put_geom, self.ema.geometry.geom)
self.assertEqual(put_props["title"], self.ema.title)
self.assertNotEqual(modified_on, self.ema.modified)
self.assertEqual(put_props["responsible"]["conservation_office"], self.ema.responsible.conservation_office)
self.assertEqual(put_props["responsible"]["conservation_file_number"], self.ema.responsible.conservation_file_number)
self.assertEqual(put_props["responsible"]["handler"], self.ema.responsible.handler)
self.assertEqual(len(put_props["actions"]), self.ema.actions.count())
self.assertEqual(len(put_props["before_states"]), self.ema.before_states.count())
self.assertEqual(len(put_props["after_states"]), self.ema.after_states.count())
self.assertEqual(len(put_props["deadlines"]), self.ema.deadlines.count())
def test_update_deduction(self):
""" Tests api update
Returns:
"""
self.deduction.intervention.share_with(self.superuser)
self.deduction.account.share_with(self.superuser)
url = reverse("api:v1:deduction", args=(str(self.deduction.id),))
json_file_path = "api/tests/v1/update/deduction_update_put_body.json"
with open(json_file_path) as json_file:
put_body = json.load(fp=json_file)
put_body["intervention"] = str(self.deduction.intervention.id)
put_body["eco_account"] = str(self.deduction.account.id)
self._test_update_object(url, put_body)
self.deduction.refresh_from_db()
self.assertEqual(put_body["intervention"], str(self.deduction.intervention.id))
self.assertEqual(put_body["eco_account"], str(self.deduction.account.id))
self.assertEqual(put_body["surface"], self.deduction.surface)

@ -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 *

@ -0,0 +1,17 @@
"""
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
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"),
]

@ -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
"""

@ -0,0 +1,34 @@
"""
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.views import EmaAPIViewV1, EcoAccountAPIViewV1, CompensationAPIViewV1, InterventionAPIViewV1, \
DeductionAPIViewV1
from api.views.views import InterventionCheckAPIView, InterventionAPIShareView, EcoAccountAPIShareView, EmaAPIShareView
app_name = "v1"
urlpatterns = [
path("intervention/<id>/check", InterventionCheckAPIView.as_view(), name="intervention-check"),
path("intervention/<id>/share", InterventionAPIShareView.as_view(), name="intervention-share"),
path("intervention/<id>", InterventionAPIViewV1.as_view(), name="intervention"),
path("intervention/", InterventionAPIViewV1.as_view(), name="intervention"),
path("compensation/<id>", CompensationAPIViewV1.as_view(), name="compensation"),
path("compensation/", CompensationAPIViewV1.as_view(), name="compensation"),
path("ecoaccount/<id>/share", EcoAccountAPIShareView.as_view(), name="ecoaccount-share"),
path("ecoaccount/<id>", EcoAccountAPIViewV1.as_view(), name="ecoaccount"),
path("ecoaccount/", EcoAccountAPIViewV1.as_view(), name="ecoaccount"),
path("deduction/<id>", DeductionAPIViewV1.as_view(), name="deduction"),
path("deduction/", DeductionAPIViewV1.as_view(), name="deduction"),
path("ema/<id>/share", EmaAPIShareView.as_view(), name="ema-share"),
path("ema/<id>", EmaAPIViewV1.as_view(), name="ema"),
path("ema/", EmaAPIViewV1.as_view(), name="ema"),
]

@ -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
"""

@ -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
"""

@ -0,0 +1,166 @@
"""
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 abc import abstractmethod
from django.contrib.gis import geos
from django.contrib.gis.geos import GEOSGeometry
from konova.utils.message_templates import DATA_UNSHARED
class AbstractModelAPISerializer:
model = 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")
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:
"""
if _id is None:
# Return all objects
del self.lookup["id"]
else:
# Return certain object
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)
"""
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
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
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)
if geometry.empty:
geometry = None
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:
"""
obj = self.model.objects.get(
id=id,
deleted__isnull=True,
)
is_shared = obj.is_shared_with(user)
if not is_shared:
raise PermissionError(DATA_UNSHARED)
return obj
@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")

@ -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
"""

@ -0,0 +1,169 @@
"""
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 api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin
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
class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin):
model = Compensation
def prepare_lookup(self, id, user):
super().prepare_lookup(id, user)
del self.lookup["users__in"]
self.lookup["intervention__users__in"] = [user]
def intervention_to_json(self, entry):
return {
"id": entry.pk,
"identifier": entry.identifier,
"title": entry.title,
}
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())
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 compensation 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)
"""
if obj.intervention is not None and 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(DATA_UNSHARED)
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
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():
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.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.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(update_action)
celery_update_parcels.delay(obj.geometry.id)
return obj.id

@ -0,0 +1,166 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 28.01.22
"""
from django.core.exceptions import ObjectDoesNotExist
from api.utils.serializer.v1.serializer import DeductableAPISerializerV1Mixin, AbstractModelAPISerializerV1
from compensation.models import EcoAccountDeduction, EcoAccount
from intervention.models import Intervention
from konova.utils.message_templates import DATA_UNSHARED
class DeductionAPISerializerV1(AbstractModelAPISerializerV1,
DeductableAPISerializerV1Mixin):
model = EcoAccountDeduction
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:
"""
super().prepare_lookup(_id, user)
del self.lookup["users__in"]
del self.lookup["deleted__isnull"]
self.lookup["intervention__users__in"] = [user]
def _model_to_geo_json(self, entry):
""" Adds the basic data
Args:
entry (): The data entry
Returns:
"""
return self._single_deduction_to_json(entry)
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
"""
acc_id = json_model["eco_account"]
intervention_id = json_model["intervention"]
surface = float(json_model["surface"])
if surface <= 0:
raise ValueError("Surface must be > 0 m²")
acc = EcoAccount.objects.get(
id=acc_id,
deleted__isnull=True,
)
intervention = Intervention.objects.get(
id=intervention_id,
deleted__isnull=True,
)
acc_shared = acc.is_shared_with(user)
intervention_shared = intervention.is_shared_with(user)
if not acc_shared:
raise PermissionError(f"Account: {DATA_UNSHARED}")
if not intervention_shared:
raise PermissionError(f"Intervention: {DATA_UNSHARED}")
deduction = self.model.objects.create(
intervention=intervention,
account=acc,
surface=surface
)
deduction.intervention.mark_as_edited(user)
return str(deduction.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:
"""
obj = self.model.objects.get(
id=id,
)
shared_with = obj.intervention.is_shared_with(user)
if not shared_with:
raise PermissionError(f"Intervention: {DATA_UNSHARED}")
return obj
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
"""
deduction = self._get_obj_from_db(id, user)
acc_id = json_model["eco_account"]
intervention_id = json_model["intervention"]
surface = float(json_model["surface"])
if surface <= 0:
raise ValueError("Surface must be > 0 m²")
acc = EcoAccount.objects.get(
id=acc_id,
deleted__isnull=True,
)
intervention = Intervention.objects.get(
id=intervention_id,
deleted__isnull=True,
)
acc_shared = acc.is_shared_with(user)
intervention_shared = intervention.is_shared_with(user)
if not acc_shared:
raise PermissionError(f"Account: {DATA_UNSHARED}")
if not intervention_shared:
raise PermissionError(f"Intervention: {DATA_UNSHARED}")
deduction.intervention = intervention
deduction.account = acc
deduction.surface = surface
deduction.save()
deduction.intervention.mark_as_edited(user)
return str(deduction.id)
def delete_entry(self, id, user):
""" Deletes the entry
Args:
id (str): The entry's id
user (User): The API user
Returns:
"""
entry = self._get_obj_from_db(id, user)
entry.intervention.mark_as_edited(user)
entry.delete()
try:
entry.refresh_from_db()
success = False
except ObjectDoesNotExist:
success = True
return success

@ -0,0 +1,186 @@
"""
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 api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, \
LegalAPISerializerV1Mixin, ResponsibilityAPISerializerV1Mixin, DeductableAPISerializerV1Mixin
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
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,
AbstractCompensationAPISerializerV1Mixin,
LegalAPISerializerV1Mixin,
ResponsibilityAPISerializerV1Mixin,
DeductableAPISerializerV1Mixin):
model = EcoAccount
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 {
"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,
}
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.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_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"]
try:
obj.deductable_surface = float(properties["deductable_surface"])
except TypeError:
raise ValueError("Deductable surface (m²) must be a number >= 0")
if obj.deductable_surface < 0:
raise ValueError("Deductable surface (m²) must be greater or equal 0")
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

@ -0,0 +1,155 @@
"""
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 api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin, \
ResponsibilityAPISerializerV1Mixin
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
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):
model = Ema
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())
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,
}
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.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 _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

@ -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
"""
from django.db import transaction
from django.db.models import QuerySet
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, \
ResponsibilityAPISerializerV1Mixin, LegalAPISerializerV1Mixin, DeductableAPISerializerV1Mixin
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,
DeductableAPISerializerV1Mixin):
model = Intervention
def _compensations_to_json(self, qs: QuerySet):
return list(
qs.values(
"id", "identifier", "title"
)
)
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)
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 _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_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 = create_action
obj.legal = legal
obj.created = created
obj.geometry = geometry
obj.responsible = resp
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)
"""
if payment_data is None:
return obj
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
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():
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"]
self._set_responsibility(obj, properties["responsible"])
self._set_legal(obj, properties["legal"])
obj.responsible.save()
obj.geometry.save()
obj.legal.save()
obj.save()
obj.users.add(user)
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 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"]
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
obj.responsible.save()
obj.geometry.save()
obj.legal.save()
obj.save()
obj.mark_as_edited(user)
celery_update_parcels.delay(obj.geometry.id)
return obj.id

@ -0,0 +1,450 @@
"""
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.contrib.gis.geos import MultiPolygon
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, 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
from konova.utils.message_templates import DATA_UNSHARED
class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
def _model_to_geo_json(self, entry):
""" Adds the basic data, which all elements hold
Args:
entry (): The data entry
Returns:
"""
if entry.geometry.geom is not None:
geom = entry.geometry.geom.geojson
else:
geom = MultiPolygon().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)
"""
if konova_code is None:
return None
return {
"atom_id": konova_code.atom_id,
"long_name": konova_code.long_name,
"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:
"""
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]
)
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 if entry.created is not None else None
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 if modified_on is not None else None
return modified_on
def delete_entry(self, id, user):
""" Marks an entry as deleted
Args:
id (str): The entry's id
user (User): The API user
Returns:
"""
entry = self._get_obj_from_db(id, user)
is_shared = entry.is_shared_with(user)
if not is_shared:
raise PermissionError(DATA_UNSHARED)
# Do not send mails if entry is deleting using API. THere could be hundreds of deletion resulting in hundreds of
# mails at once.
entry.mark_as_deleted(user, send_mail=False)
entry.refresh_from_db()
success = entry.deleted is not None
return success
class DeductableAPISerializerV1Mixin:
class Meta:
abstract = True
def _single_deduction_to_json(self, entry):
""" Serializes a single eco account deduction into json
Args:
entry (EcoAccountDeduction): An EcoAccountDeduction
Returns:
serialized_json (dict)
"""
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,
}
}
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 [
self._single_deduction_to_json(entry)
for entry in qs
]
class ResponsibilityAPISerializerV1Mixin:
class Meta:
abstract = True
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 _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
class LegalAPISerializerV1Mixin:
class Meta:
abstract = True
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 _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:
abstract = True
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)
"""
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}")
# 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=deadlines
).first()
if pre_existing_deadline is not None:
deadlines.append(pre_existing_deadline.id)
else:
# Create and add id to list
new_deadline = Deadline.objects.create(
type=deadline_type,
date=date,
comment=comment,
)
deadlines.append(new_deadline.id)
obj.deadlines.set(deadlines)
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)
"""
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")
# 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=states
).first()
if pre_existing_state is not None:
states.append(pre_existing_state.id)
else:
# 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.append(new_state.id)
states_manager.set(states)
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)
"""
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}")
# 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=actions
).first()
if pre_existing_action is not None:
actions.append(pre_existing_action.id)
else:
# 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,
)
actions.append(new_action.id)
obj.actions.set(actions)
return obj
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",
))

@ -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 *

@ -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

@ -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
"""

@ -0,0 +1,132 @@
"""
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.utils.serializer.v1.compensation import CompensationAPISerializerV1
from api.utils.serializer.v1.deduction import DeductionAPISerializerV1
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 AbstractAPIView
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
Performs the fetching and serialization of the data
Args:
request (HttpRequest): The incoming request
id (str): The entries id (optional)
Returns:
response (JsonResponse)
"""
try:
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):
""" 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)
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})
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:
response (JsonResponse)
"""
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})
def delete(self, request: HttpRequest, id=None):
""" Handles a DELETE request
Args:
request (HttpRequest): The incoming request
id (str): The object's id
Returns:
response (JsonResponse)
"""
try:
success = self.serializer.delete_entry(id, self.user)
except Exception as e:
return self.return_error_response(e, 500)
return JsonResponse(
{
"success": success,
}
)
class InterventionAPIViewV1(AbstractAPIViewV1):
serializer = InterventionAPISerializerV1
class CompensationAPIViewV1(AbstractAPIViewV1):
serializer = CompensationAPISerializerV1
class EcoAccountAPIViewV1(AbstractAPIViewV1):
serializer = EcoAccountAPISerializerV1
class EmaAPIViewV1(AbstractAPIViewV1):
serializer = EmaAPISerializerV1
class DeductionAPIViewV1(AbstractAPIViewV1):
serializer = DeductionAPISerializerV1

@ -0,0 +1,270 @@
"""
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.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, KSP_USER_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 konova.utils.user_checks import is_default_group_only
from user.models import User
class AbstractAPIView(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
"""
user = None
class Meta:
abstract = True
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
try:
# Fetch the proper user from the given request header token
ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
self.user = APIUserToken.get_user_from_token(ksp_token, ksp_user)
if not self.user.is_default_user():
raise PermissionError("Default permissions required")
except PermissionError as e:
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
)
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
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))
if is_default_group_only(self.user):
# Default only users are not allowed to remove other users from having access. They can only add new ones!
new_users_to_be_added = User.objects.filter(
username__in=new_users
).exclude(
id__in=obj.shared_users
)
new_users_objs = obj.shared_users.union(new_users_to_be_added)
obj.share_with_list(new_users_objs)
return True
class InterventionAPIShareView(AbstractModelShareAPIView):
model = Intervention
class EcoAccountAPIShareView(AbstractModelShareAPIView):
model = EcoAccount
class EmaAPIShareView(AbstractModelShareAPIView):
model = Ema

@ -29,6 +29,7 @@ class CompensationAdmin(BaseObjectAdmin):
"identifier", "identifier",
"title", "title",
"created", "created",
"deleted",
] ]

@ -35,9 +35,7 @@ class CompensationManager(models.Manager):
""" """
def get_queryset(self): def get_queryset(self):
return super().get_queryset().filter( return super().get_queryset().select_related(
deleted__isnull=True,
).select_related(
"modified", "modified",
"intervention", "intervention",
"intervention__recorded", "intervention__recorded",

@ -245,6 +245,38 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
# Compensations inherit their shared state from the interventions # Compensations inherit their shared state from the interventions
return self.intervention.is_shared_with(user) 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
Returns:
users (QuerySet)
"""
return self.intervention.users.all()
def get_LANIS_link(self) -> str: def get_LANIS_link(self) -> str:
""" Generates a link for LANIS depending on the geometry """ Generates a link for LANIS depending on the geometry

@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
identifier = tmp.generate_new_identifier() identifier = tmp.generate_new_identifier()
return JsonResponse( return JsonResponse(
data={ data={
"identifier": identifier "gen_data": identifier
} }
) )

@ -118,7 +118,7 @@ def new_id_view(request: HttpRequest):
identifier = tmp.generate_new_identifier() identifier = tmp.generate_new_identifier()
return JsonResponse( return JsonResponse(
data={ data={
"identifier": identifier "gen_data": identifier
} }
) )

@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
identifier = tmp.generate_new_identifier() identifier = tmp.generate_new_identifier()
return JsonResponse( return JsonResponse(
data={ data={
"identifier": identifier "gen_data": identifier
} }
) )

@ -116,7 +116,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
z_l = v_zoom z_l = v_zoom
break break
zoom_lvl = z_l zoom_lvl = z_l
except AttributeError: except (AttributeError, IndexError) as e:
# If no geometry has been added, yet. # If no geometry has been added, yet.
x = 1 x = 1
y = 1 y = 1
@ -154,6 +154,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
def set_unrecorded(self, user: User): def set_unrecorded(self, user: User):
log_entry = super().set_unrecorded(user) log_entry = super().set_unrecorded(user)
self.add_log_entry_to_compensations(log_entry) self.add_log_entry_to_compensations(log_entry)
return log_entry
def set_recorded(self, user: User) -> UserActionLogEntry: def set_recorded(self, user: User) -> UserActionLogEntry:
log_entry = super().set_recorded(user) log_entry = super().set_recorded(user)
@ -259,11 +260,6 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
Returns: 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) super().mark_as_edited(performing_user, request)
if self.checked: if self.checked:
self.set_unchecked() self.set_unchecked()

@ -360,19 +360,21 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
self.eco_account.recorded = rec_action self.eco_account.recorded = rec_action
self.eco_account.share_with_list([self.superuser]) self.eco_account.share_with_list([self.superuser])
self.eco_account.save() self.eco_account.save()
num_all_deducs = EcoAccountDeduction.objects.count()
# Run the request # Run the request
self.client_user.post(new_url, post_data) self.client_user.post(new_url, post_data)
# Expect the deduction to be created, since all constraints are fulfilled # Expect the deduction to be created, since all constraints are fulfilled
self.assertEqual(1, self.eco_account.deductions.count()) self.assertEqual(1, self.eco_account.deductions.count())
self.assertEqual(1, EcoAccountDeduction.objects.count()) self.assertEqual(num_all_deducs + 1, EcoAccountDeduction.objects.count())
# Make sure the deduction contains the expected data # Make sure the deduction contains the expected data
deduction = EcoAccountDeduction.objects.first() deduction = EcoAccountDeduction.objects.get(
account=self.eco_account,
intervention=self.intervention
)
self.assertEqual(deduction.surface, test_surface) self.assertEqual(deduction.surface, test_surface)
self.assertEqual(deduction.intervention, self.intervention)
self.assertEqual(deduction.account, self.eco_account)
# Return deduction for further usage in tests # Return deduction for further usage in tests
return deduction return deduction

@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
identifier = tmp_intervention.generate_new_identifier() identifier = tmp_intervention.generate_new_identifier()
return JsonResponse( return JsonResponse(
data={ data={
"identifier": identifier "gen_data": identifier
} }
) )

@ -75,9 +75,7 @@ def default_group_required(function):
@wraps(function) @wraps(function)
def wrap(request, *args, **kwargs): def wrap(request, *args, **kwargs):
user = request.user user = request.user
has_group = user.groups.filter( has_group = user.is_default_user()
name=DEFAULT_GROUP
).exists()
if has_group: if has_group:
return function(request, *args, **kwargs) return function(request, *args, **kwargs)
else: else:
@ -95,9 +93,7 @@ def registration_office_group_required(function):
@wraps(function) @wraps(function)
def wrap(request, *args, **kwargs): def wrap(request, *args, **kwargs):
user = request.user user = request.user
has_group = user.groups.filter( has_group = user.is_zb_user()
name=ZB_GROUP
).exists()
if has_group: if has_group:
return function(request, *args, **kwargs) return function(request, *args, **kwargs)
else: else:
@ -115,9 +111,7 @@ def conservation_office_group_required(function):
@wraps(function) @wraps(function)
def wrap(request, *args, **kwargs): def wrap(request, *args, **kwargs):
user = request.user user = request.user
has_group = user.groups.filter( has_group = user.is_ets_user()
name=ETS_GROUP
).exists()
if has_group: if has_group:
return function(request, *args, **kwargs) return function(request, *args, **kwargs)
else: else:

@ -10,7 +10,7 @@ from ema.models import Ema
from intervention.models import Intervention from intervention.models import Intervention
from konova.management.commands.setup import BaseKonovaCommand from konova.management.commands.setup import BaseKonovaCommand
from konova.models import Deadline, Geometry, Parcel, District from konova.models import Deadline, Geometry, Parcel, District
from user.models import UserActionLogEntry from user.models import UserActionLogEntry, UserAction
class Command(BaseKonovaCommand): class Command(BaseKonovaCommand):
@ -55,7 +55,11 @@ class Command(BaseKonovaCommand):
""" """
self._write_warning("=== Sanitize log entries ===") 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) intervention_log_entries_ids = self.get_all_log_entries_ids(Intervention)
attached_log_entries_id = intervention_log_entries_ids.union( attached_log_entries_id = intervention_log_entries_ids.union(

@ -10,6 +10,7 @@ import uuid
from abc import abstractmethod from abc import abstractmethod
from django.contrib import messages 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, \ 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, \ celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \
@ -103,7 +104,7 @@ class BaseObject(BaseResource):
def set_status_messages(self, request: HttpRequest): def set_status_messages(self, request: HttpRequest):
raise NotImplementedError raise NotImplementedError
def mark_as_deleted(self, user: User): def mark_as_deleted(self, user: User, send_mail: bool = True):
""" Mark an entry as deleted """ Mark an entry as deleted
Does not delete from database but sets a timestamp for being deleted on and which user deleted the object Does not delete from database but sets a timestamp for being deleted on and which user deleted the object
@ -123,8 +124,9 @@ class BaseObject(BaseResource):
self.deleted = action self.deleted = action
self.log.add(action) self.log.add(action)
if send_mail:
# Send mail # 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: for user_id in shared_users:
celery_send_mail_shared_data_deleted.delay(self.identifier, user_id) celery_send_mail_shared_data_deleted.delay(self.identifier, user_id)
@ -276,7 +278,8 @@ class RecordableObjectMixin(models.Model):
self.save() self.save()
if self.recorded: if self.recorded:
self.set_unrecorded(performing_user) action = self.set_unrecorded(performing_user)
self.log.add(action)
if request: if request:
messages.info( messages.info(
request, request,
@ -464,6 +467,15 @@ class ShareableObjectMixin(models.Model):
# Set new shared users # Set new shared users
self.share_with_list(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): class GeoReferencedMixin(models.Model):
geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL) geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL)

@ -70,6 +70,7 @@ INSTALLED_APPS = [
'ema', 'ema',
'codelist', 'codelist',
'analysis', 'analysis',
'api',
] ]
if DEBUG: if DEBUG:
INSTALLED_APPS += [ INSTALLED_APPS += [
@ -213,6 +214,7 @@ if DEBUG:
DEFAULT_FROM_EMAIL = "no-reply@ksp.de" # The default email address for the 'from' element DEFAULT_FROM_EMAIL = "no-reply@ksp.de" # The default email address for the 'from' element
EMAIL_HOST = os.environ.get('SMTP_HOST'), EMAIL_HOST = os.environ.get('SMTP_HOST'),
EMAIL_REPLY_TO = os.environ.get('SMTP_REAL_REPLY_MAIL') EMAIL_REPLY_TO = os.environ.get('SMTP_REAL_REPLY_MAIL')
SUPPORT_MAIL_RECIPIENT = EMAIL_REPLY_TO
EMAIL_PORT = os.environ.get('SMTP_PORT'), EMAIL_PORT = os.environ.get('SMTP_PORT'),
# LOGGING # LOGGING

@ -1,9 +1,9 @@
{% load i18n fontawesome_5 %} {% load i18n fontawesome_5 %}
<div class="input-group w-100" title="{{ widget.value|stringformat:'s' }}"> <div class="input-group w-100" title="{{ widget.value|stringformat:'s' }}">
<input id="gen-id-input" aria-describedby="gen-id-btn" type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}> <input id="gen-data-input" aria-describedby="gen-data-btn" type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}>
<div class="input-group-append" onclick="fetchNewIdentifier()"> <div class="input-group-append" onclick="fetchNewIdentifier()">
<span id="gen-id-btn" class="btn btn-default" value="{% trans 'Generate new' %}" title="{% trans 'Generate new' %}">{% fa5_icon 'dice' %}</span> <span id="gen-data-btn" class="btn btn-default" value="{% trans 'Generate new' %}" title="{% trans 'Generate new' %}">{% fa5_icon 'dice' %}</span>
</div> </div>
</div> </div>
<script> <script>
@ -13,7 +13,7 @@
return response.json(); return response.json();
}) })
.then(function(data){ .then(function(data){
document.getElementById("gen-id-input").value = data["identifier"]; document.getElementById("gen-data-input").value = data["gen_data"];
}) })
.catch(function(error){ .catch(function(error){
console.log(error); console.log(error);

@ -7,6 +7,7 @@ Created on: 26.10.21
""" """
import datetime import datetime
from ema.models import Ema
from user.models import User from user.models import User
from django.contrib.auth.models import Group from django.contrib.auth.models import Group
from django.contrib.gis.geos import MultiPolygon, Polygon from django.contrib.gis.geos import MultiPolygon, Polygon
@ -15,7 +16,7 @@ from django.test import TestCase, Client
from django.urls import reverse from django.urls import reverse
from codelist.models import KonovaCode from codelist.models import KonovaCode
from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount, EcoAccountDeduction
from intervention.models import Legal, Responsibility, Intervention from intervention.models import Legal, Responsibility, Intervention
from konova.management.commands.setup_data import GROUPS_DATA from konova.management.commands.setup_data import GROUPS_DATA
from konova.models import Geometry from konova.models import Geometry
@ -52,6 +53,8 @@ class BaseTestCase(TestCase):
cls.intervention = cls.create_dummy_intervention() cls.intervention = cls.create_dummy_intervention()
cls.compensation = cls.create_dummy_compensation() cls.compensation = cls.create_dummy_compensation()
cls.eco_account = cls.create_dummy_eco_account() cls.eco_account = cls.create_dummy_eco_account()
cls.ema = cls.create_dummy_ema()
cls.deduction = cls.create_dummy_deduction()
cls.create_dummy_states() cls.create_dummy_states()
cls.create_dummy_action() cls.create_dummy_action()
cls.codes = cls.create_dummy_codes() cls.codes = cls.create_dummy_codes()
@ -168,6 +171,38 @@ class BaseTestCase(TestCase):
) )
return eco_account return eco_account
@classmethod
def create_dummy_ema(cls):
""" Creates an ema which can be used for tests
Returns:
"""
# Create dummy data
# Create log entry
action = UserActionLogEntry.get_created_action(cls.superuser)
geometry = Geometry.objects.create()
# Create responsible data object
responsible_data = Responsibility.objects.create()
# Finally create main object, holding the other objects
ema = Ema.objects.create(
identifier="TEST",
title="Test_title",
responsible=responsible_data,
created=action,
geometry=geometry,
comment="Test",
)
return ema
@classmethod
def create_dummy_deduction(cls):
return EcoAccountDeduction.objects.create(
account=cls.create_dummy_eco_account(),
intervention=cls.create_dummy_intervention(),
surface=100,
)
@classmethod @classmethod
def create_dummy_states(cls): def create_dummy_states(cls):
""" Creates an intervention which can be used for tests """ Creates an intervention which can be used for tests

@ -38,6 +38,7 @@ urlpatterns = [
path('news/', include("news.urls")), path('news/', include("news.urls")),
path('cl/', include("codelist.urls")), path('cl/', include("codelist.urls")),
path('analysis/', include("analysis.urls")), path('analysis/', include("analysis.urls")),
path('api/', include("api.urls")),
# Generic deadline routes # Generic deadline routes
path('deadline/<id>/remove', remove_deadline_view, name="deadline-remove"), path('deadline/<id>/remove', remove_deadline_view, name="deadline-remove"),

@ -13,6 +13,19 @@ import qrcode.image.svg
from io import BytesIO 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: 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 Generates a random string of variable length

@ -11,7 +11,7 @@ from django.core.mail import send_mail
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from konova.sub_settings.django_settings import DEFAULT_FROM_EMAIL, EMAIL_REPLY_TO from konova.sub_settings.django_settings import DEFAULT_FROM_EMAIL, EMAIL_REPLY_TO, SUPPORT_MAIL_RECIPIENT
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@ -181,3 +181,24 @@ class Mailer:
msg msg
) )
def send_mail_verify_api_token(self, user):
""" Send a mail if a user creates a new token
Args:
user (User): The user, having a new api token
Returns:
"""
context = {
"user": user,
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
}
msg = render_to_string("email/api/verify_token.html", context)
user_mail_address = [SUPPORT_MAIL_RECIPIENT]
self.send(
user_mail_address,
_("Request for new API token"),
msg
)

Binary file not shown.

@ -20,13 +20,13 @@
#: konova/filters/mixins.py:385 konova/filters/mixins.py:386 #: konova/filters/mixins.py:385 konova/filters/mixins.py:386
#: konova/forms.py:140 konova/forms.py:241 konova/forms.py:312 #: konova/forms.py:140 konova/forms.py:241 konova/forms.py:312
#: konova/forms.py:339 konova/forms.py:349 konova/forms.py:362 #: konova/forms.py:339 konova/forms.py:349 konova/forms.py:362
#: konova/forms.py:374 konova/forms.py:392 user/forms.py:38 #: konova/forms.py:374 konova/forms.py:392 user/forms.py:42
#, fuzzy #, fuzzy
msgid "" msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2022-01-20 12:30+0100\n" "POT-Creation-Date: 2022-01-28 16:27+0100\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@ -317,6 +317,7 @@ msgid "Identifier"
msgstr "Kennung" msgstr "Kennung"
#: compensation/forms/forms.py:35 intervention/forms/forms.py:29 #: compensation/forms/forms.py:35 intervention/forms/forms.py:29
#: user/forms.py:126
msgid "Generated automatically" msgid "Generated automatically"
msgstr "Automatisch generiert" msgstr "Automatisch generiert"
@ -1716,11 +1717,11 @@ msgstr "Kontrolle am"
msgid "Other" msgid "Other"
msgstr "Sonstige" msgstr "Sonstige"
#: konova/sub_settings/django_settings.py:154 #: konova/sub_settings/django_settings.py:155
msgid "German" msgid "German"
msgstr "" msgstr ""
#: konova/sub_settings/django_settings.py:155 #: konova/sub_settings/django_settings.py:156
msgid "English" msgid "English"
msgstr "" msgstr ""
@ -1803,6 +1804,10 @@ msgstr "{} - Freigegebene Daten gelöscht"
msgid "{} - Shared data checked" msgid "{} - Shared data checked"
msgstr "{} - Freigegebene Daten geprüft" msgstr "{} - Freigegebene Daten geprüft"
#: konova/utils/mailer.py:201 templates/email/api/verify_token.html:4
msgid "Request for new API token"
msgstr "Anfrage für neuen API Token"
#: konova/utils/message_templates.py:11 #: konova/utils/message_templates.py:11
msgid "There was an error on this form." msgid "There was an error on this form."
msgstr "Es gab einen Fehler im Formular." msgstr "Es gab einen Fehler im Formular."
@ -1937,6 +1942,33 @@ msgstr ""
msgid "Something happened. We are working on it!" msgid "Something happened. We are working on it!"
msgstr "Irgendetwas ist passiert. Wir arbeiten daran!" msgstr "Irgendetwas ist passiert. Wir arbeiten daran!"
#: templates/email/api/verify_token.html:7
msgid "Hello support"
msgstr "Hallo Support"
#: templates/email/api/verify_token.html:9
msgid "you need to verify the API token for user"
msgstr "Sie müssen einen API Token für folgenden Nutzer freischalten"
#: templates/email/api/verify_token.html:15
msgid ""
"If unsure, please contact the user. The API token can not be used until you "
"activated it in the admin backend."
msgstr ""
"Falls Sie sich unsicher sind, kontaktieren Sie den Nutzer vorher. Der API "
"Token kann so lange nicht verwendet werden, wie er noch nicht von Ihnen im "
"Admin Backend aktiviert worden ist."
#: templates/email/api/verify_token.html:18
#: templates/email/checking/shared_data_checked.html:17
#: templates/email/deleting/shared_data_deleted.html:17
#: templates/email/recording/shared_data_recorded.html:17
#: templates/email/recording/shared_data_unrecorded.html:17
#: templates/email/sharing/shared_access_given.html:18
#: templates/email/sharing/shared_access_removed.html:18
msgid "Best regards"
msgstr "Beste Grüße"
#: templates/email/checking/shared_data_checked.html:4 #: templates/email/checking/shared_data_checked.html:4
msgid "Shared data checked" msgid "Shared data checked"
msgstr "Freigegebene Daten geprüft" msgstr "Freigegebene Daten geprüft"
@ -1962,15 +1994,6 @@ msgstr ""
"Das bedeutet, dass die zuständige Zulassungsbehörde die Korrektheit des " "Das bedeutet, dass die zuständige Zulassungsbehörde die Korrektheit des "
"Datensatzes soeben bestätigt hat." "Datensatzes soeben bestätigt hat."
#: templates/email/checking/shared_data_checked.html:17
#: templates/email/deleting/shared_data_deleted.html:17
#: templates/email/recording/shared_data_recorded.html:17
#: templates/email/recording/shared_data_unrecorded.html:17
#: templates/email/sharing/shared_access_given.html:18
#: templates/email/sharing/shared_access_removed.html:18
msgid "Best regards"
msgstr "Beste Grüße"
#: templates/email/deleting/shared_data_deleted.html:4 #: templates/email/deleting/shared_data_deleted.html:4
msgid "Shared data deleted" msgid "Shared data deleted"
msgstr "Freigegebene Daten gelöscht" msgstr "Freigegebene Daten gelöscht"
@ -2214,39 +2237,51 @@ msgid ""
" " " "
msgstr "" msgstr ""
"\n" "\n"
" Diese Daten sind noch nicht veröffentlicht und " " Diese Daten sind noch nicht veröffentlicht und können daher "
"können daher aktuell nicht eingesehen werden. Schauen Sie zu einem späteren " "aktuell nicht eingesehen werden. Schauen Sie zu einem späteren Zeitpunkt "
"Zeitpunkt wieder vorbei. \n" "wieder vorbei. \n"
" " " "
#: user/forms.py:23 #: user/forms.py:27
msgid "Notifications" msgid "Notifications"
msgstr "Benachrichtigungen" msgstr "Benachrichtigungen"
#: user/forms.py:25 #: user/forms.py:29
msgid "Select the situations when you want to receive a notification" msgid "Select the situations when you want to receive a notification"
msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?" msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?"
#: user/forms.py:37 #: user/forms.py:41
msgid "Edit notifications" msgid "Edit notifications"
msgstr "Benachrichtigungen bearbeiten" msgstr "Benachrichtigungen bearbeiten"
#: user/forms.py:72 user/templates/user/index.html:9 #: user/forms.py:76 user/templates/user/index.html:9
msgid "Username" msgid "Username"
msgstr "Nutzername" msgstr "Nutzername"
#: user/forms.py:83 #: user/forms.py:87
msgid "Person name" msgid "Person name"
msgstr "Name" msgstr "Name"
#: user/forms.py:94 user/templates/user/index.html:17 #: user/forms.py:98 user/templates/user/index.html:17
msgid "E-Mail" msgid "E-Mail"
msgstr "" msgstr ""
#: user/forms.py:108 #: user/forms.py:112
msgid "User contact data" msgid "User contact data"
msgstr "Kontaktdaten" msgstr "Kontaktdaten"
#: user/forms.py:122
msgid "Token"
msgstr ""
#: user/forms.py:137
msgid "Create new token"
msgstr "Neuen Token generieren"
#: user/forms.py:138
msgid "A new token needs to be validated by an administrator!"
msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!"
#: user/models/user_action.py:20 #: user/models/user_action.py:20
msgid "Unrecorded" msgid "Unrecorded"
msgstr "Entzeichnet" msgstr "Entzeichnet"
@ -2300,18 +2335,58 @@ msgstr "Benachrichtigungseinstellungen ändern"
msgid "Notification settings" msgid "Notification settings"
msgstr "Benachrichtigungen" msgstr "Benachrichtigungen"
#: user/views.py:29 #: user/templates/user/index.html:58
msgid "See or edit your API token"
msgstr "API token einsehen oder neu generieren"
#: user/templates/user/index.html:61
msgid "API"
msgstr ""
#: user/templates/user/token.html:6
msgid "API settings"
msgstr "API Einstellungen"
#: user/templates/user/token.html:10
msgid "Current token"
msgstr "Aktueller Token"
#: user/templates/user/token.html:14
msgid "Authenticated by admins"
msgstr "Von Admin freigeschaltet"
#: user/templates/user/token.html:18
msgid "Token has been verified and can be used"
msgstr "Token wurde freigeschaltet und kann verwendet werden"
#: user/templates/user/token.html:20
msgid "Token waiting for verification"
msgstr "Token noch nicht freigeschaltet"
#: user/templates/user/token.html:24
msgid "Valid until"
msgstr "Läuft ab am"
#: user/views.py:31
msgid "User settings" msgid "User settings"
msgstr "Einstellungen" msgstr "Einstellungen"
#: user/views.py:55 #: user/views.py:57
msgid "Notifications edited" msgid "Notifications edited"
msgstr "Benachrichtigungen bearbeitet" msgstr "Benachrichtigungen bearbeitet"
#: user/views.py:67 #: user/views.py:69
msgid "User notifications" msgid "User notifications"
msgstr "Benachrichtigungen" msgstr "Benachrichtigungen"
#: user/views.py:92
msgid "New token generated. Administrators need to validate."
msgstr "Neuer Token generiert. Administratoren sind informiert."
#: user/views.py:103
msgid "User API token"
msgstr "API Nutzer Token"
#: venv/lib/python3.7/site-packages/bootstrap4/components.py:17 #: venv/lib/python3.7/site-packages/bootstrap4/components.py:17
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3 #: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/messages.html:4 #: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/messages.html:4
@ -3056,7 +3131,7 @@ msgstr ""
#: venv/lib/python3.7/site-packages/django/forms/fields.py:54 #: venv/lib/python3.7/site-packages/django/forms/fields.py:54
msgid "This field is required." msgid "This field is required."
msgstr "" msgstr "Pflichtfeld"
#: venv/lib/python3.7/site-packages/django/forms/fields.py:247 #: venv/lib/python3.7/site-packages/django/forms/fields.py:247
msgid "Enter a whole number." msgid "Enter a whole number."

@ -0,0 +1,23 @@
{% load i18n %}
<div>
<h2>{% trans 'Request for new API token' %}</h2>
<hr>
<article>
{% trans 'Hello support' %},
<br>
{% trans 'you need to verify the API token for user' %}:
<br>
<br>
<strong>{{user.username}}</strong>
<br>
<br>
{% trans 'If unsure, please contact the user. The API token can not be used until you activated it in the admin backend.' %}
<br>
<br>
{% trans 'Best regards' %}
<br>
KSP
</article>
</div>

@ -6,8 +6,12 @@ Created on: 08.07.21
""" """
from django import forms from django import forms
from django.urls import reverse from django.db import IntegrityError
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from api.models import APIUserToken
from intervention.inputs import GenerateInput
from user.models import User from user.models import User
from konova.forms import BaseForm, BaseModalForm from konova.forms import BaseForm, BaseModalForm
@ -113,3 +117,46 @@ class UserContactForm(BaseModalForm):
self.initialize_form_field("mail", self.instance.email) self.initialize_form_field("mail", self.instance.email)
class UserAPITokenForm(BaseForm):
token = forms.CharField(
label=_("Token"),
label_suffix="",
max_length=255,
required=True,
help_text=_("Generated automatically"),
widget=GenerateInput(
attrs={
"class": "form-control",
"url": reverse_lazy("api:generate-new-token"),
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Create new token")
self.form_caption = _("A new token needs to be validated by an administrator!")
self.action_url = reverse("user:api-token")
self.cancel_redirect = reverse("user:index")
# Make direct token editing by user impossible. Instead set the proper url for generating a new token
self.initialize_form_field("token", None)
self.fields["token"].widget.attrs["readonly"] = True
def save(self):
""" Saves the form data
Returns:
api_token (APIUserToken)
"""
user = self.instance
new_token = self.cleaned_data["token"]
if user.api_token is not None:
user.api_token.delete()
new_token = APIUserToken.objects.create(
token=new_token
)
user.api_token = new_token
user.save()
return new_token

@ -9,18 +9,57 @@ from django.contrib.auth.models import AbstractUser
from django.db import models from django.db import models
from api.models import APIUserToken
from konova.settings import ZB_GROUP, DEFAULT_GROUP, ETS_GROUP
from konova.utils.mailer import Mailer from konova.utils.mailer import Mailer
from user.enums import UserNotificationEnum from user.enums import UserNotificationEnum
class User(AbstractUser): class User(AbstractUser):
notifications = models.ManyToManyField("user.UserNotification", related_name="+", blank=True) 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): def is_notification_setting_set(self, notification_enum: UserNotificationEnum):
return self.notifications.filter( return self.notifications.filter(
id=notification_enum.value id=notification_enum.value
).exists() ).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): def send_mail_shared_access_removed(self, obj_identifier):
""" Sends a mail to the user in case of removed shared access """ Sends a mail to the user in case of removed shared access
@ -104,3 +143,19 @@ class User(AbstractUser):
if notification_set: if notification_set:
mailer = Mailer() mailer = Mailer()
mailer.send_mail_shared_data_checked(obj_identifier, self) mailer.send_mail_shared_data_checked(obj_identifier, self)
def get_API_token(self):
""" Getter for an API token
Creates a new one if none exists, yet.
Returns:
token (APIUserToken)
"""
if self.api_token is None:
token = APIUserToken.objects.create()
self.api_token = token
self.save()
else:
token = self.api_token
return token

@ -54,6 +54,14 @@
</button> </button>
</a> </a>
</div> </div>
<div class="row mb-2">
<a href="{% url 'user:api-token' %}" title="{% trans 'See or edit your API token' %}">
<button class="btn btn-default">
{% fa5_icon 'code' %}
<span>{% trans 'API' %}</span>
</button>
</a>
</div>
</div> </div>
</div> </div>
</div> </div>

@ -0,0 +1,33 @@
{% extends 'base.html' %}
{% load i18n fontawesome_5 %}
{% block body %}
<div class="container">
<h3>{% trans 'API settings' %}</h3>
<div class="table-container">
<table class="table table-hover">
<tr>
<th scope="row">{% trans 'Current token' %}</th>
<td>{{ user.api_token.token }}</td>
</tr>
<tr>
<th scope="row">{% trans 'Authenticated by admins' %}</th>
{% if user.api_token is None %}
<td></td>
{% elif user.api_token.is_active %}
<td class="text-success" title="{% trans 'Token has been verified and can be used' %}">{% fa5_icon 'check-circle' %}</td>
{% else %}
<td class="text-primary" title="{% trans 'Token waiting for verification' %}">{% fa5_icon 'hourglass-half' %}</td>
{% endif %}
</tr>
<tr>
<th scope="row">{% trans 'Valid until' %}</th>
<td>{{ user.api_token.valid_until|default_if_none:"-" }}</td>
</tr>
</table>
</div>
</div>
<hr>
{% include 'form/table/generic_table_form.html' %}
{% endblock %}

@ -13,6 +13,7 @@ app_name = "user"
urlpatterns = [ urlpatterns = [
path("", index_view, name="index"), path("", index_view, name="index"),
path("notifications/", notifications_view, name="notifications"), path("notifications/", notifications_view, name="notifications"),
path("token/api", api_token_view, name="api-token"),
path("contact/<id>", contact_view, name="contact"), path("contact/<id>", contact_view, name="contact"),
] ]

@ -2,14 +2,16 @@ from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.mailer import Mailer
from konova.utils.message_templates import FORM_INVALID
from user.models import User from user.models import User
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import any_group_check from konova.decorators import any_group_check, default_group_required
from user.forms import UserNotificationForm, UserContactForm from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm
@login_required @login_required
@ -70,6 +72,40 @@ def notifications_view(request: HttpRequest):
return render(request, template, context) return render(request, template, context)
@login_required
@default_group_required
def api_token_view(request: HttpRequest):
""" Handles the request for user api frontend settings
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "user/token.html"
user = request.user
form = UserAPITokenForm(request.POST or None, instance=user)
if request.method == "POST":
if form.is_valid():
token = form.save()
messages.info(request, _("New token generated. Administrators need to validate."))
mailer = Mailer()
mailer.send_mail_verify_api_token(user)
return redirect("user:api-token")
else:
messages.error(request, FORM_INVALID, extra_tags="danger")
elif request.method != "GET":
raise NotImplementedError
context = {
"user": user,
"form": form,
TAB_TITLE_IDENTIFIER: _("User API token"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required @login_required
def contact_view(request: HttpRequest, id: str): def contact_view(request: HttpRequest, id: str):
""" Renders contact modal view of a users contact data """ Renders contact modal view of a users contact data

Loading…
Cancel
Save