Merge branch 'master' into Docker
# Conflicts: # konova/sub_settings/django_settings.py
This commit is contained in:
commit
5b7351e331
0
api/__init__.py
Normal file
0
api/__init__.py
Normal file
16
api/admin.py
Normal file
16
api/admin.py
Normal file
@ -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)
|
5
api/apps.py
Normal file
5
api/apps.py
Normal file
@ -0,0 +1,5 @@
|
||||
from django.apps import AppConfig
|
||||
|
||||
|
||||
class ApiConfig(AppConfig):
|
||||
name = 'api'
|
8
api/models/__init__.py
Normal file
8
api/models/__init__.py
Normal file
@ -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 *
|
50
api/models/token.py
Normal file
50
api/models/token.py
Normal file
@ -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
|
9
api/settings.py
Normal file
9
api/settings.py
Normal file
@ -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"
|
7
api/tests/__init__.py
Normal file
7
api/tests/__init__.py
Normal file
@ -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
|
||||
|
||||
"""
|
7
api/tests/v1/__init__.py
Normal file
7
api/tests/v1/__init__.py
Normal file
@ -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
|
||||
|
||||
"""
|
7
api/tests/v1/create/__init__.py
Normal file
7
api/tests/v1/create/__init__.py
Normal file
@ -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
|
||||
|
||||
"""
|
19
api/tests/v1/create/compensation_create_post_body.json
Normal file
19
api/tests/v1/create/compensation_create_post_body.json
Normal file
@ -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": [
|
||||
]
|
||||
}
|
||||
}
|
5
api/tests/v1/create/deduction_create_post_body.json
Normal file
5
api/tests/v1/create/deduction_create_post_body.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"eco_account": "CHANGE_BEFORE_RUN!!!",
|
||||
"surface": 500.0,
|
||||
"intervention": "CHANGE_BEFORE_RUN!!!"
|
||||
}
|
25
api/tests/v1/create/ecoaccount_create_post_body.json
Normal file
25
api/tests/v1/create/ecoaccount_create_post_body.json
Normal file
@ -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": [
|
||||
]
|
||||
}
|
||||
}
|
21
api/tests/v1/create/ema_create_post_body.json
Normal file
21
api/tests/v1/create/ema_create_post_body.json
Normal file
@ -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": [
|
||||
]
|
||||
}
|
||||
}
|
21
api/tests/v1/create/intervention_create_post_body.json
Normal file
21
api/tests/v1/create/intervention_create_post_body.json
Normal file
@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
122
api/tests/v1/create/test_api_create.py
Normal file
122
api/tests/v1/create/test_api_create.py
Normal file
@ -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)
|
||||
|
7
api/tests/v1/delete/__init__.py
Normal file
7
api/tests/v1/delete/__init__.py
Normal file
@ -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
|
||||
|
||||
"""
|
118
api/tests/v1/delete/test_api_delete.py
Normal file
118
api/tests/v1/delete/test_api_delete.py
Normal file
@ -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
|
||||
|
7
api/tests/v1/get/__init__.py
Normal file
7
api/tests/v1/get/__init__.py
Normal file
@ -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
|
||||
|
||||
"""
|
187
api/tests/v1/get/test_api_get.py
Normal file
187
api/tests/v1/get/test_api_get.py
Normal file
@ -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)
|
||||
|
7
api/tests/v1/share/__init__.py
Normal file
7
api/tests/v1/share/__init__.py
Normal file
@ -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
|
||||
|
||||
"""
|
153
api/tests/v1/share/test_api_sharing.py
Normal file
153
api/tests/v1/share/test_api_sharing.py
Normal file
@ -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)
|
7
api/tests/v1/update/__init__.py
Normal file
7
api/tests/v1/update/__init__.py
Normal file
@ -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
|
||||
|
||||
"""
|
61
api/tests/v1/update/compensation_update_put_body.json
Normal file
61
api/tests/v1/update/compensation_update_put_body.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
5
api/tests/v1/update/deduction_update_put_body.json
Normal file
5
api/tests/v1/update/deduction_update_put_body.json
Normal file
@ -0,0 +1,5 @@
|
||||
{
|
||||
"eco_account": "CHANGE_BEFORE_RUN!!!",
|
||||
"surface": 523400.0,
|
||||
"intervention": "CHANGE_BEFORE_RUN!!!"
|
||||
}
|
70
api/tests/v1/update/ecoaccount_update_put_body.json
Normal file
70
api/tests/v1/update/ecoaccount_update_put_body.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
63
api/tests/v1/update/ema_update_put_body.json
Normal file
63
api/tests/v1/update/ema_update_put_body.json
Normal file
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
61
api/tests/v1/update/intervention_update_put_body.json
Normal file
61
api/tests/v1/update/intervention_update_put_body.json
Normal file
@ -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": []
|
||||
}
|
||||
}
|
||||
}
|
186
api/tests/v1/update/test_api_update.py
Normal file
186
api/tests/v1/update/test_api_update.py
Normal file
@ -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)
|
8
api/urls/__init__.py
Normal file
8
api/urls/__init__.py
Normal file
@ -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 *
|
17
api/urls/urls.py
Normal file
17
api/urls/urls.py
Normal file
@ -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"),
|
||||
]
|
7
api/urls/v1/__init__.py
Normal file
7
api/urls/v1/__init__.py
Normal file
@ -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
|
||||
|
||||
"""
|
34
api/urls/v1/urls.py
Normal file
34
api/urls/v1/urls.py
Normal file
@ -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"),
|
||||
]
|
7
api/utils/__init__.py
Normal file
7
api/utils/__init__.py
Normal file
@ -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
|
||||
|
||||
"""
|
7
api/utils/serializer/__init__.py
Normal file
7
api/utils/serializer/__init__.py
Normal file
@ -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
|
||||
|
||||
"""
|
166
api/utils/serializer/serializer.py
Normal file
166
api/utils/serializer/serializer.py
Normal file
@ -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")
|
7
api/utils/serializer/v1/__init__.py
Normal file
7
api/utils/serializer/v1/__init__.py
Normal file
@ -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
|
||||
|
||||
"""
|
169
api/utils/serializer/v1/compensation.py
Normal file
169
api/utils/serializer/v1/compensation.py
Normal file
@ -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
|
166
api/utils/serializer/v1/deduction.py
Normal file
166
api/utils/serializer/v1/deduction.py
Normal file
@ -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
|
186
api/utils/serializer/v1/ecoaccount.py
Normal file
186
api/utils/serializer/v1/ecoaccount.py
Normal file
@ -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
|
155
api/utils/serializer/v1/ema.py
Normal file
155
api/utils/serializer/v1/ema.py
Normal file
@ -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
|
200
api/utils/serializer/v1/intervention.py
Normal file
200
api/utils/serializer/v1/intervention.py
Normal file
@ -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
|
450
api/utils/serializer/v1/serializer.py
Normal file
450
api/utils/serializer/v1/serializer.py
Normal file
@ -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",
|
||||
))
|
8
api/views/__init__.py
Normal file
8
api/views/__init__.py
Normal file
@ -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 *
|
35
api/views/method_views.py
Normal file
35
api/views/method_views.py
Normal file
@ -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
|
7
api/views/v1/__init__.py
Normal file
7
api/views/v1/__init__.py
Normal file
@ -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
|
||||
|
||||
"""
|
132
api/views/v1/views.py
Normal file
132
api/views/v1/views.py
Normal file
@ -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
|
270
api/views/views.py
Normal file
270
api/views/views.py
Normal file
@ -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",
|
||||
"title",
|
||||
"created",
|
||||
"deleted",
|
||||
]
|
||||
|
||||
|
||||
|
@ -35,9 +35,7 @@ class CompensationManager(models.Manager):
|
||||
|
||||
"""
|
||||
def get_queryset(self):
|
||||
return super().get_queryset().filter(
|
||||
deleted__isnull=True,
|
||||
).select_related(
|
||||
return super().get_queryset().select_related(
|
||||
"modified",
|
||||
"intervention",
|
||||
"intervention__recorded",
|
||||
|
@ -245,6 +245,38 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
|
||||
# Compensations inherit their shared state from the interventions
|
||||
return self.intervention.is_shared_with(user)
|
||||
|
||||
def share_with(self, user: User):
|
||||
""" Adds user to list of shared access users
|
||||
|
||||
Args:
|
||||
user (User): The user to be added to the object
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if not self.intervention.is_shared_with(user):
|
||||
self.intervention.users.add(user)
|
||||
|
||||
def share_with_list(self, user_list: list):
|
||||
""" Sets the list of shared access users
|
||||
|
||||
Args:
|
||||
user_list (list): The users to be added to the object
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
self.intervention.users.set(user_list)
|
||||
|
||||
@property
|
||||
def shared_users(self) -> QuerySet:
|
||||
""" Shortcut for fetching the users which have shared access on this object
|
||||
|
||||
Returns:
|
||||
users (QuerySet)
|
||||
"""
|
||||
return self.intervention.users.all()
|
||||
|
||||
def get_LANIS_link(self) -> str:
|
||||
""" Generates a link for LANIS depending on the geometry
|
||||
|
||||
|
@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
|
||||
identifier = tmp.generate_new_identifier()
|
||||
return JsonResponse(
|
||||
data={
|
||||
"identifier": identifier
|
||||
"gen_data": identifier
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -118,7 +118,7 @@ def new_id_view(request: HttpRequest):
|
||||
identifier = tmp.generate_new_identifier()
|
||||
return JsonResponse(
|
||||
data={
|
||||
"identifier": identifier
|
||||
"gen_data": identifier
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
|
||||
identifier = tmp.generate_new_identifier()
|
||||
return JsonResponse(
|
||||
data={
|
||||
"identifier": identifier
|
||||
"gen_data": identifier
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -116,7 +116,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
|
||||
z_l = v_zoom
|
||||
break
|
||||
zoom_lvl = z_l
|
||||
except AttributeError:
|
||||
except (AttributeError, IndexError) as e:
|
||||
# If no geometry has been added, yet.
|
||||
x = 1
|
||||
y = 1
|
||||
@ -154,6 +154,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
|
||||
def set_unrecorded(self, user: User):
|
||||
log_entry = super().set_unrecorded(user)
|
||||
self.add_log_entry_to_compensations(log_entry)
|
||||
return log_entry
|
||||
|
||||
def set_recorded(self, user: User) -> UserActionLogEntry:
|
||||
log_entry = super().set_recorded(user)
|
||||
@ -259,11 +260,6 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
|
||||
Returns:
|
||||
|
||||
"""
|
||||
user_action_edit = UserActionLogEntry.get_edited_action(performing_user, comment=edit_comment)
|
||||
self.log.add(user_action_edit)
|
||||
self.modified = user_action_edit
|
||||
self.save()
|
||||
|
||||
super().mark_as_edited(performing_user, request)
|
||||
if self.checked:
|
||||
self.set_unchecked()
|
||||
|
@ -360,19 +360,21 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
|
||||
self.eco_account.recorded = rec_action
|
||||
self.eco_account.share_with_list([self.superuser])
|
||||
self.eco_account.save()
|
||||
num_all_deducs = EcoAccountDeduction.objects.count()
|
||||
|
||||
# Run the request
|
||||
self.client_user.post(new_url, post_data)
|
||||
|
||||
# Expect the deduction to be created, since all constraints are fulfilled
|
||||
self.assertEqual(1, self.eco_account.deductions.count())
|
||||
self.assertEqual(1, EcoAccountDeduction.objects.count())
|
||||
self.assertEqual(num_all_deducs + 1, EcoAccountDeduction.objects.count())
|
||||
|
||||
# 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.intervention, self.intervention)
|
||||
self.assertEqual(deduction.account, self.eco_account)
|
||||
|
||||
# Return deduction for further usage in tests
|
||||
return deduction
|
||||
|
@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
|
||||
identifier = tmp_intervention.generate_new_identifier()
|
||||
return JsonResponse(
|
||||
data={
|
||||
"identifier": identifier
|
||||
"gen_data": identifier
|
||||
}
|
||||
)
|
||||
|
||||
|
@ -75,9 +75,7 @@ def default_group_required(function):
|
||||
@wraps(function)
|
||||
def wrap(request, *args, **kwargs):
|
||||
user = request.user
|
||||
has_group = user.groups.filter(
|
||||
name=DEFAULT_GROUP
|
||||
).exists()
|
||||
has_group = user.is_default_user()
|
||||
if has_group:
|
||||
return function(request, *args, **kwargs)
|
||||
else:
|
||||
@ -95,9 +93,7 @@ def registration_office_group_required(function):
|
||||
@wraps(function)
|
||||
def wrap(request, *args, **kwargs):
|
||||
user = request.user
|
||||
has_group = user.groups.filter(
|
||||
name=ZB_GROUP
|
||||
).exists()
|
||||
has_group = user.is_zb_user()
|
||||
if has_group:
|
||||
return function(request, *args, **kwargs)
|
||||
else:
|
||||
@ -115,9 +111,7 @@ def conservation_office_group_required(function):
|
||||
@wraps(function)
|
||||
def wrap(request, *args, **kwargs):
|
||||
user = request.user
|
||||
has_group = user.groups.filter(
|
||||
name=ETS_GROUP
|
||||
).exists()
|
||||
has_group = user.is_ets_user()
|
||||
if has_group:
|
||||
return function(request, *args, **kwargs)
|
||||
else:
|
||||
|
@ -10,7 +10,7 @@ from ema.models import Ema
|
||||
from intervention.models import Intervention
|
||||
from konova.management.commands.setup import BaseKonovaCommand
|
||||
from konova.models import Deadline, Geometry, Parcel, District
|
||||
from user.models import UserActionLogEntry
|
||||
from user.models import UserActionLogEntry, UserAction
|
||||
|
||||
|
||||
class Command(BaseKonovaCommand):
|
||||
@ -55,7 +55,11 @@ class Command(BaseKonovaCommand):
|
||||
|
||||
"""
|
||||
self._write_warning("=== Sanitize log entries ===")
|
||||
all_log_entries = UserActionLogEntry.objects.all()
|
||||
# Exclude created log entries from being cleaned, since they can be part of objects which do not have logs
|
||||
# Being in a log (or not) is essential for this cleanup
|
||||
all_log_entries = UserActionLogEntry.objects.all().exclude(
|
||||
action=UserAction.CREATED
|
||||
)
|
||||
|
||||
intervention_log_entries_ids = self.get_all_log_entries_ids(Intervention)
|
||||
attached_log_entries_id = intervention_log_entries_ids.union(
|
||||
|
@ -10,6 +10,7 @@ import uuid
|
||||
from abc import abstractmethod
|
||||
|
||||
from django.contrib import messages
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from konova.tasks import celery_send_mail_shared_access_removed, celery_send_mail_shared_access_given, \
|
||||
celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \
|
||||
@ -103,7 +104,7 @@ class BaseObject(BaseResource):
|
||||
def set_status_messages(self, request: HttpRequest):
|
||||
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
|
||||
|
||||
Does not delete from database but sets a timestamp for being deleted on and which user deleted the object
|
||||
@ -123,10 +124,11 @@ class BaseObject(BaseResource):
|
||||
self.deleted = action
|
||||
self.log.add(action)
|
||||
|
||||
# Send mail
|
||||
shared_users = self.users.all().values_list("id", flat=True)
|
||||
for user_id in shared_users:
|
||||
celery_send_mail_shared_data_deleted.delay(self.identifier, user_id)
|
||||
if send_mail:
|
||||
# Send mail
|
||||
shared_users = self.shared_users.values_list("id", flat=True)
|
||||
for user_id in shared_users:
|
||||
celery_send_mail_shared_data_deleted.delay(self.identifier, user_id)
|
||||
|
||||
self.save()
|
||||
|
||||
@ -276,7 +278,8 @@ class RecordableObjectMixin(models.Model):
|
||||
self.save()
|
||||
|
||||
if self.recorded:
|
||||
self.set_unrecorded(performing_user)
|
||||
action = self.set_unrecorded(performing_user)
|
||||
self.log.add(action)
|
||||
if request:
|
||||
messages.info(
|
||||
request,
|
||||
@ -464,6 +467,15 @@ class ShareableObjectMixin(models.Model):
|
||||
# Set new shared users
|
||||
self.share_with_list(users)
|
||||
|
||||
@property
|
||||
def shared_users(self) -> QuerySet:
|
||||
""" Shortcut for fetching the users which have shared access on this object
|
||||
|
||||
Returns:
|
||||
users (QuerySet)
|
||||
"""
|
||||
return self.users.all()
|
||||
|
||||
|
||||
class GeoReferencedMixin(models.Model):
|
||||
geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL)
|
||||
|
@ -70,6 +70,7 @@ INSTALLED_APPS = [
|
||||
'ema',
|
||||
'codelist',
|
||||
'analysis',
|
||||
'api',
|
||||
]
|
||||
if DEBUG:
|
||||
INSTALLED_APPS += [
|
||||
@ -213,6 +214,7 @@ if DEBUG:
|
||||
DEFAULT_FROM_EMAIL = "no-reply@ksp.de" # The default email address for the 'from' element
|
||||
EMAIL_HOST = os.environ.get('SMTP_HOST'),
|
||||
EMAIL_REPLY_TO = os.environ.get('SMTP_REAL_REPLY_MAIL')
|
||||
SUPPORT_MAIL_RECIPIENT = EMAIL_REPLY_TO
|
||||
EMAIL_PORT = os.environ.get('SMTP_PORT'),
|
||||
|
||||
# LOGGING
|
||||
|
@ -1,9 +1,9 @@
|
||||
{% load i18n fontawesome_5 %}
|
||||
|
||||
<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()">
|
||||
<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>
|
||||
<script>
|
||||
@ -13,7 +13,7 @@
|
||||
return response.json();
|
||||
})
|
||||
.then(function(data){
|
||||
document.getElementById("gen-id-input").value = data["identifier"];
|
||||
document.getElementById("gen-data-input").value = data["gen_data"];
|
||||
})
|
||||
.catch(function(error){
|
||||
console.log(error);
|
||||
|
@ -7,6 +7,7 @@ Created on: 26.10.21
|
||||
"""
|
||||
import datetime
|
||||
|
||||
from ema.models import Ema
|
||||
from user.models import User
|
||||
from django.contrib.auth.models import Group
|
||||
from django.contrib.gis.geos import MultiPolygon, Polygon
|
||||
@ -15,7 +16,7 @@ from django.test import TestCase, Client
|
||||
from django.urls import reverse
|
||||
|
||||
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 konova.management.commands.setup_data import GROUPS_DATA
|
||||
from konova.models import Geometry
|
||||
@ -52,6 +53,8 @@ class BaseTestCase(TestCase):
|
||||
cls.intervention = cls.create_dummy_intervention()
|
||||
cls.compensation = cls.create_dummy_compensation()
|
||||
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_action()
|
||||
cls.codes = cls.create_dummy_codes()
|
||||
@ -168,6 +171,38 @@ class BaseTestCase(TestCase):
|
||||
)
|
||||
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
|
||||
def create_dummy_states(cls):
|
||||
""" Creates an intervention which can be used for tests
|
||||
|
@ -38,6 +38,7 @@ urlpatterns = [
|
||||
path('news/', include("news.urls")),
|
||||
path('cl/', include("codelist.urls")),
|
||||
path('analysis/', include("analysis.urls")),
|
||||
path('api/', include("api.urls")),
|
||||
|
||||
# Generic deadline routes
|
||||
path('deadline/<id>/remove', remove_deadline_view, name="deadline-remove"),
|
||||
|
@ -13,6 +13,19 @@ import qrcode.image.svg
|
||||
from io import BytesIO
|
||||
|
||||
|
||||
def generate_token() -> str:
|
||||
""" Shortcut for default generating of e.g. API token
|
||||
|
||||
Returns:
|
||||
token (str)
|
||||
"""
|
||||
return generate_random_string(
|
||||
length=64,
|
||||
use_numbers=True,
|
||||
use_letters_lc=True
|
||||
)
|
||||
|
||||
|
||||
def generate_random_string(length: int, use_numbers: bool = False, use_letters_lc: bool = False, use_letters_uc: bool = False) -> str:
|
||||
"""
|
||||
Generates a random string of variable length
|
||||
|
@ -11,7 +11,7 @@ from django.core.mail import send_mail
|
||||
from django.template.loader import render_to_string
|
||||
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__)
|
||||
|
||||
@ -181,3 +181,24 @@ class Mailer:
|
||||
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/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:374 konova/forms.py:392 user/forms.py:38
|
||||
#: konova/forms.py:374 konova/forms.py:392 user/forms.py:42
|
||||
#, fuzzy
|
||||
msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\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"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@ -317,6 +317,7 @@ msgid "Identifier"
|
||||
msgstr "Kennung"
|
||||
|
||||
#: compensation/forms/forms.py:35 intervention/forms/forms.py:29
|
||||
#: user/forms.py:126
|
||||
msgid "Generated automatically"
|
||||
msgstr "Automatisch generiert"
|
||||
|
||||
@ -1716,11 +1717,11 @@ msgstr "Kontrolle am"
|
||||
msgid "Other"
|
||||
msgstr "Sonstige"
|
||||
|
||||
#: konova/sub_settings/django_settings.py:154
|
||||
#: konova/sub_settings/django_settings.py:155
|
||||
msgid "German"
|
||||
msgstr ""
|
||||
|
||||
#: konova/sub_settings/django_settings.py:155
|
||||
#: konova/sub_settings/django_settings.py:156
|
||||
msgid "English"
|
||||
msgstr ""
|
||||
|
||||
@ -1803,6 +1804,10 @@ msgstr "{} - Freigegebene Daten gelöscht"
|
||||
msgid "{} - Shared data checked"
|
||||
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
|
||||
msgid "There was an error on this form."
|
||||
msgstr "Es gab einen Fehler im Formular."
|
||||
@ -1937,6 +1942,33 @@ msgstr ""
|
||||
msgid "Something happened. We are working on it!"
|
||||
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
|
||||
msgid "Shared data checked"
|
||||
msgstr "Freigegebene Daten geprüft"
|
||||
@ -1962,15 +1994,6 @@ msgstr ""
|
||||
"Das bedeutet, dass die zuständige Zulassungsbehörde die Korrektheit des "
|
||||
"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
|
||||
msgid "Shared data deleted"
|
||||
msgstr "Freigegebene Daten gelöscht"
|
||||
@ -2214,39 +2237,51 @@ msgid ""
|
||||
" "
|
||||
msgstr ""
|
||||
"\n"
|
||||
" Diese Daten sind noch nicht veröffentlicht und "
|
||||
"können daher aktuell nicht eingesehen werden. Schauen Sie zu einem späteren "
|
||||
"Zeitpunkt wieder vorbei. \n"
|
||||
" Diese Daten sind noch nicht veröffentlicht und können daher "
|
||||
"aktuell nicht eingesehen werden. Schauen Sie zu einem späteren Zeitpunkt "
|
||||
"wieder vorbei. \n"
|
||||
" "
|
||||
|
||||
#: user/forms.py:23
|
||||
#: user/forms.py:27
|
||||
msgid "Notifications"
|
||||
msgstr "Benachrichtigungen"
|
||||
|
||||
#: user/forms.py:25
|
||||
#: user/forms.py:29
|
||||
msgid "Select the situations when you want to receive a notification"
|
||||
msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?"
|
||||
|
||||
#: user/forms.py:37
|
||||
#: user/forms.py:41
|
||||
msgid "Edit notifications"
|
||||
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"
|
||||
msgstr "Nutzername"
|
||||
|
||||
#: user/forms.py:83
|
||||
#: user/forms.py:87
|
||||
msgid "Person 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"
|
||||
msgstr ""
|
||||
|
||||
#: user/forms.py:108
|
||||
#: user/forms.py:112
|
||||
msgid "User contact data"
|
||||
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
|
||||
msgid "Unrecorded"
|
||||
msgstr "Entzeichnet"
|
||||
@ -2300,18 +2335,58 @@ msgstr "Benachrichtigungseinstellungen ändern"
|
||||
msgid "Notification settings"
|
||||
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"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: user/views.py:55
|
||||
#: user/views.py:57
|
||||
msgid "Notifications edited"
|
||||
msgstr "Benachrichtigungen bearbeitet"
|
||||
|
||||
#: user/views.py:67
|
||||
#: user/views.py:69
|
||||
msgid "User notifications"
|
||||
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/templates/bootstrap4/form_errors.html:3
|
||||
#: 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
|
||||
msgid "This field is required."
|
||||
msgstr ""
|
||||
msgstr "Pflichtfeld"
|
||||
|
||||
#: venv/lib/python3.7/site-packages/django/forms/fields.py:247
|
||||
msgid "Enter a whole number."
|
||||
|
23
templates/email/api/verify_token.html
Normal file
23
templates/email/api/verify_token.html
Normal file
@ -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.urls import reverse
|
||||
from django.db import IntegrityError
|
||||
from django.urls import reverse, reverse_lazy
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from api.models import APIUserToken
|
||||
from intervention.inputs import GenerateInput
|
||||
from user.models import User
|
||||
|
||||
from konova.forms import BaseForm, BaseModalForm
|
||||
@ -113,3 +117,46 @@ class UserContactForm(BaseModalForm):
|
||||
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 api.models import APIUserToken
|
||||
from konova.settings import ZB_GROUP, DEFAULT_GROUP, ETS_GROUP
|
||||
from konova.utils.mailer import Mailer
|
||||
from user.enums import UserNotificationEnum
|
||||
|
||||
|
||||
class User(AbstractUser):
|
||||
notifications = models.ManyToManyField("user.UserNotification", related_name="+", blank=True)
|
||||
api_token = models.OneToOneField(
|
||||
"api.APIUserToken",
|
||||
blank=True,
|
||||
null=True,
|
||||
help_text="The user's API token",
|
||||
on_delete=models.SET_NULL
|
||||
)
|
||||
|
||||
def is_notification_setting_set(self, notification_enum: UserNotificationEnum):
|
||||
return self.notifications.filter(
|
||||
id=notification_enum.value
|
||||
).exists()
|
||||
|
||||
def is_zb_user(self):
|
||||
""" Shortcut for checking whether a user is of a special group or not
|
||||
|
||||
Returns:
|
||||
bool
|
||||
"""
|
||||
return self.groups.filter(
|
||||
name=ZB_GROUP
|
||||
).exists()
|
||||
|
||||
def is_default_user(self):
|
||||
""" Shortcut for checking whether a user is of a special group or not
|
||||
|
||||
Returns:
|
||||
bool
|
||||
"""
|
||||
return self.groups.filter(
|
||||
name=DEFAULT_GROUP
|
||||
).exists()
|
||||
|
||||
def is_ets_user(self):
|
||||
""" Shortcut for checking whether a user is of a special group or not
|
||||
|
||||
Returns:
|
||||
bool
|
||||
"""
|
||||
return self.groups.filter(
|
||||
name=ETS_GROUP
|
||||
).exists()
|
||||
|
||||
def send_mail_shared_access_removed(self, obj_identifier):
|
||||
""" Sends a mail to the user in case of removed shared access
|
||||
|
||||
@ -104,3 +143,19 @@ class User(AbstractUser):
|
||||
if notification_set:
|
||||
mailer = Mailer()
|
||||
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>
|
||||
</a>
|
||||
</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>
|
||||
|
33
user/templates/user/token.html
Normal file
33
user/templates/user/token.html
Normal file
@ -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 = [
|
||||
path("", index_view, name="index"),
|
||||
path("notifications/", notifications_view, name="notifications"),
|
||||
path("token/api", api_token_view, name="api-token"),
|
||||
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 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 django.http import HttpRequest
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import any_group_check
|
||||
from user.forms import UserNotificationForm, UserContactForm
|
||||
from konova.decorators import any_group_check, default_group_required
|
||||
from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm
|
||||
|
||||
|
||||
@login_required
|
||||
@ -70,6 +72,40 @@ def notifications_view(request: HttpRequest):
|
||||
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
|
||||
def contact_view(request: HttpRequest, id: str):
|
||||
""" Renders contact modal view of a users contact data
|
||||
|
Loading…
Reference in New Issue
Block a user