Merge pull request '31_API' (#90) from 31_API into master
Reviewed-on: SGD-Nord/konova#90
This commit is contained in:
commit
9c338f8a90
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",
|
"identifier",
|
||||||
"title",
|
"title",
|
||||||
"created",
|
"created",
|
||||||
|
"deleted",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -35,9 +35,7 @@ class CompensationManager(models.Manager):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
return super().get_queryset().filter(
|
return super().get_queryset().select_related(
|
||||||
deleted__isnull=True,
|
|
||||||
).select_related(
|
|
||||||
"modified",
|
"modified",
|
||||||
"intervention",
|
"intervention",
|
||||||
"intervention__recorded",
|
"intervention__recorded",
|
||||||
|
@ -245,6 +245,38 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin):
|
|||||||
# Compensations inherit their shared state from the interventions
|
# Compensations inherit their shared state from the interventions
|
||||||
return self.intervention.is_shared_with(user)
|
return self.intervention.is_shared_with(user)
|
||||||
|
|
||||||
|
def share_with(self, user: User):
|
||||||
|
""" Adds user to list of shared access users
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (User): The user to be added to the object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not self.intervention.is_shared_with(user):
|
||||||
|
self.intervention.users.add(user)
|
||||||
|
|
||||||
|
def share_with_list(self, user_list: list):
|
||||||
|
""" Sets the list of shared access users
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user_list (list): The users to be added to the object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
self.intervention.users.set(user_list)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shared_users(self) -> QuerySet:
|
||||||
|
""" Shortcut for fetching the users which have shared access on this object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
users (QuerySet)
|
||||||
|
"""
|
||||||
|
return self.intervention.users.all()
|
||||||
|
|
||||||
def get_LANIS_link(self) -> str:
|
def get_LANIS_link(self) -> str:
|
||||||
""" Generates a link for LANIS depending on the geometry
|
""" Generates a link for LANIS depending on the geometry
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
|
|||||||
identifier = tmp.generate_new_identifier()
|
identifier = tmp.generate_new_identifier()
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
data={
|
data={
|
||||||
"identifier": identifier
|
"gen_data": identifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -118,7 +118,7 @@ def new_id_view(request: HttpRequest):
|
|||||||
identifier = tmp.generate_new_identifier()
|
identifier = tmp.generate_new_identifier()
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
data={
|
data={
|
||||||
"identifier": identifier
|
"gen_data": identifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
|
|||||||
identifier = tmp.generate_new_identifier()
|
identifier = tmp.generate_new_identifier()
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
data={
|
data={
|
||||||
"identifier": identifier
|
"gen_data": identifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -116,7 +116,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
|
|||||||
z_l = v_zoom
|
z_l = v_zoom
|
||||||
break
|
break
|
||||||
zoom_lvl = z_l
|
zoom_lvl = z_l
|
||||||
except AttributeError:
|
except (AttributeError, IndexError) as e:
|
||||||
# If no geometry has been added, yet.
|
# If no geometry has been added, yet.
|
||||||
x = 1
|
x = 1
|
||||||
y = 1
|
y = 1
|
||||||
@ -154,6 +154,7 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
|
|||||||
def set_unrecorded(self, user: User):
|
def set_unrecorded(self, user: User):
|
||||||
log_entry = super().set_unrecorded(user)
|
log_entry = super().set_unrecorded(user)
|
||||||
self.add_log_entry_to_compensations(log_entry)
|
self.add_log_entry_to_compensations(log_entry)
|
||||||
|
return log_entry
|
||||||
|
|
||||||
def set_recorded(self, user: User) -> UserActionLogEntry:
|
def set_recorded(self, user: User) -> UserActionLogEntry:
|
||||||
log_entry = super().set_recorded(user)
|
log_entry = super().set_recorded(user)
|
||||||
@ -259,11 +260,6 @@ class Intervention(BaseObject, ShareableObjectMixin, RecordableObjectMixin, Chec
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
user_action_edit = UserActionLogEntry.get_edited_action(performing_user, comment=edit_comment)
|
|
||||||
self.log.add(user_action_edit)
|
|
||||||
self.modified = user_action_edit
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
super().mark_as_edited(performing_user, request)
|
super().mark_as_edited(performing_user, request)
|
||||||
if self.checked:
|
if self.checked:
|
||||||
self.set_unchecked()
|
self.set_unchecked()
|
||||||
|
@ -360,19 +360,21 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
self.eco_account.recorded = rec_action
|
self.eco_account.recorded = rec_action
|
||||||
self.eco_account.share_with_list([self.superuser])
|
self.eco_account.share_with_list([self.superuser])
|
||||||
self.eco_account.save()
|
self.eco_account.save()
|
||||||
|
num_all_deducs = EcoAccountDeduction.objects.count()
|
||||||
|
|
||||||
# Run the request
|
# Run the request
|
||||||
self.client_user.post(new_url, post_data)
|
self.client_user.post(new_url, post_data)
|
||||||
|
|
||||||
# Expect the deduction to be created, since all constraints are fulfilled
|
# Expect the deduction to be created, since all constraints are fulfilled
|
||||||
self.assertEqual(1, self.eco_account.deductions.count())
|
self.assertEqual(1, self.eco_account.deductions.count())
|
||||||
self.assertEqual(1, EcoAccountDeduction.objects.count())
|
self.assertEqual(num_all_deducs + 1, EcoAccountDeduction.objects.count())
|
||||||
|
|
||||||
# Make sure the deduction contains the expected data
|
# Make sure the deduction contains the expected data
|
||||||
deduction = EcoAccountDeduction.objects.first()
|
deduction = EcoAccountDeduction.objects.get(
|
||||||
|
account=self.eco_account,
|
||||||
|
intervention=self.intervention
|
||||||
|
)
|
||||||
self.assertEqual(deduction.surface, test_surface)
|
self.assertEqual(deduction.surface, test_surface)
|
||||||
self.assertEqual(deduction.intervention, self.intervention)
|
|
||||||
self.assertEqual(deduction.account, self.eco_account)
|
|
||||||
|
|
||||||
# Return deduction for further usage in tests
|
# Return deduction for further usage in tests
|
||||||
return deduction
|
return deduction
|
||||||
|
@ -108,7 +108,7 @@ def new_id_view(request: HttpRequest):
|
|||||||
identifier = tmp_intervention.generate_new_identifier()
|
identifier = tmp_intervention.generate_new_identifier()
|
||||||
return JsonResponse(
|
return JsonResponse(
|
||||||
data={
|
data={
|
||||||
"identifier": identifier
|
"gen_data": identifier
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -75,9 +75,7 @@ def default_group_required(function):
|
|||||||
@wraps(function)
|
@wraps(function)
|
||||||
def wrap(request, *args, **kwargs):
|
def wrap(request, *args, **kwargs):
|
||||||
user = request.user
|
user = request.user
|
||||||
has_group = user.groups.filter(
|
has_group = user.is_default_user()
|
||||||
name=DEFAULT_GROUP
|
|
||||||
).exists()
|
|
||||||
if has_group:
|
if has_group:
|
||||||
return function(request, *args, **kwargs)
|
return function(request, *args, **kwargs)
|
||||||
else:
|
else:
|
||||||
@ -95,9 +93,7 @@ def registration_office_group_required(function):
|
|||||||
@wraps(function)
|
@wraps(function)
|
||||||
def wrap(request, *args, **kwargs):
|
def wrap(request, *args, **kwargs):
|
||||||
user = request.user
|
user = request.user
|
||||||
has_group = user.groups.filter(
|
has_group = user.is_zb_user()
|
||||||
name=ZB_GROUP
|
|
||||||
).exists()
|
|
||||||
if has_group:
|
if has_group:
|
||||||
return function(request, *args, **kwargs)
|
return function(request, *args, **kwargs)
|
||||||
else:
|
else:
|
||||||
@ -115,9 +111,7 @@ def conservation_office_group_required(function):
|
|||||||
@wraps(function)
|
@wraps(function)
|
||||||
def wrap(request, *args, **kwargs):
|
def wrap(request, *args, **kwargs):
|
||||||
user = request.user
|
user = request.user
|
||||||
has_group = user.groups.filter(
|
has_group = user.is_ets_user()
|
||||||
name=ETS_GROUP
|
|
||||||
).exists()
|
|
||||||
if has_group:
|
if has_group:
|
||||||
return function(request, *args, **kwargs)
|
return function(request, *args, **kwargs)
|
||||||
else:
|
else:
|
||||||
|
@ -10,7 +10,7 @@ from ema.models import Ema
|
|||||||
from intervention.models import Intervention
|
from intervention.models import Intervention
|
||||||
from konova.management.commands.setup import BaseKonovaCommand
|
from konova.management.commands.setup import BaseKonovaCommand
|
||||||
from konova.models import Deadline, Geometry, Parcel, District
|
from konova.models import Deadline, Geometry, Parcel, District
|
||||||
from user.models import UserActionLogEntry
|
from user.models import UserActionLogEntry, UserAction
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseKonovaCommand):
|
class Command(BaseKonovaCommand):
|
||||||
@ -55,7 +55,11 @@ class Command(BaseKonovaCommand):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
self._write_warning("=== Sanitize log entries ===")
|
self._write_warning("=== Sanitize log entries ===")
|
||||||
all_log_entries = UserActionLogEntry.objects.all()
|
# Exclude created log entries from being cleaned, since they can be part of objects which do not have logs
|
||||||
|
# Being in a log (or not) is essential for this cleanup
|
||||||
|
all_log_entries = UserActionLogEntry.objects.all().exclude(
|
||||||
|
action=UserAction.CREATED
|
||||||
|
)
|
||||||
|
|
||||||
intervention_log_entries_ids = self.get_all_log_entries_ids(Intervention)
|
intervention_log_entries_ids = self.get_all_log_entries_ids(Intervention)
|
||||||
attached_log_entries_id = intervention_log_entries_ids.union(
|
attached_log_entries_id = intervention_log_entries_ids.union(
|
||||||
|
@ -10,6 +10,7 @@ import uuid
|
|||||||
from abc import abstractmethod
|
from abc import abstractmethod
|
||||||
|
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
from konova.tasks import celery_send_mail_shared_access_removed, celery_send_mail_shared_access_given, \
|
from konova.tasks import celery_send_mail_shared_access_removed, celery_send_mail_shared_access_given, \
|
||||||
celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \
|
celery_send_mail_shared_data_recorded, celery_send_mail_shared_data_unrecorded, \
|
||||||
@ -103,7 +104,7 @@ class BaseObject(BaseResource):
|
|||||||
def set_status_messages(self, request: HttpRequest):
|
def set_status_messages(self, request: HttpRequest):
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def mark_as_deleted(self, user: User):
|
def mark_as_deleted(self, user: User, send_mail: bool = True):
|
||||||
""" Mark an entry as deleted
|
""" Mark an entry as deleted
|
||||||
|
|
||||||
Does not delete from database but sets a timestamp for being deleted on and which user deleted the object
|
Does not delete from database but sets a timestamp for being deleted on and which user deleted the object
|
||||||
@ -123,10 +124,11 @@ class BaseObject(BaseResource):
|
|||||||
self.deleted = action
|
self.deleted = action
|
||||||
self.log.add(action)
|
self.log.add(action)
|
||||||
|
|
||||||
# Send mail
|
if send_mail:
|
||||||
shared_users = self.users.all().values_list("id", flat=True)
|
# Send mail
|
||||||
for user_id in shared_users:
|
shared_users = self.shared_users.values_list("id", flat=True)
|
||||||
celery_send_mail_shared_data_deleted.delay(self.identifier, user_id)
|
for user_id in shared_users:
|
||||||
|
celery_send_mail_shared_data_deleted.delay(self.identifier, user_id)
|
||||||
|
|
||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
@ -276,7 +278,8 @@ class RecordableObjectMixin(models.Model):
|
|||||||
self.save()
|
self.save()
|
||||||
|
|
||||||
if self.recorded:
|
if self.recorded:
|
||||||
self.set_unrecorded(performing_user)
|
action = self.set_unrecorded(performing_user)
|
||||||
|
self.log.add(action)
|
||||||
if request:
|
if request:
|
||||||
messages.info(
|
messages.info(
|
||||||
request,
|
request,
|
||||||
@ -464,6 +467,15 @@ class ShareableObjectMixin(models.Model):
|
|||||||
# Set new shared users
|
# Set new shared users
|
||||||
self.share_with_list(users)
|
self.share_with_list(users)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def shared_users(self) -> QuerySet:
|
||||||
|
""" Shortcut for fetching the users which have shared access on this object
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
users (QuerySet)
|
||||||
|
"""
|
||||||
|
return self.users.all()
|
||||||
|
|
||||||
|
|
||||||
class GeoReferencedMixin(models.Model):
|
class GeoReferencedMixin(models.Model):
|
||||||
geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL)
|
geometry = models.ForeignKey("konova.Geometry", null=True, blank=True, on_delete=models.SET_NULL)
|
||||||
|
@ -70,6 +70,7 @@ INSTALLED_APPS = [
|
|||||||
'ema',
|
'ema',
|
||||||
'codelist',
|
'codelist',
|
||||||
'analysis',
|
'analysis',
|
||||||
|
'api',
|
||||||
]
|
]
|
||||||
if DEBUG:
|
if DEBUG:
|
||||||
INSTALLED_APPS += [
|
INSTALLED_APPS += [
|
||||||
@ -212,6 +213,7 @@ EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location
|
|||||||
DEFAULT_FROM_EMAIL = "service@ksp.de" # The default email address for the 'from' element
|
DEFAULT_FROM_EMAIL = "service@ksp.de" # The default email address for the 'from' element
|
||||||
EMAIL_HOST = "localhost"
|
EMAIL_HOST = "localhost"
|
||||||
EMAIL_REPLY_TO = "ksp-servicestelle@sgdnord.rlp.de"
|
EMAIL_REPLY_TO = "ksp-servicestelle@sgdnord.rlp.de"
|
||||||
|
SUPPORT_MAIL_RECIPIENT = EMAIL_REPLY_TO
|
||||||
EMAIL_PORT = "25"
|
EMAIL_PORT = "25"
|
||||||
#EMAIL_HOST_USER = ""
|
#EMAIL_HOST_USER = ""
|
||||||
#EMAIL_HOST_PASSWORD = ""
|
#EMAIL_HOST_PASSWORD = ""
|
||||||
|
@ -1,9 +1,9 @@
|
|||||||
{% load i18n fontawesome_5 %}
|
{% load i18n fontawesome_5 %}
|
||||||
|
|
||||||
<div class="input-group w-100" title="{{ widget.value|stringformat:'s' }}">
|
<div class="input-group w-100" title="{{ widget.value|stringformat:'s' }}">
|
||||||
<input id="gen-id-input" aria-describedby="gen-id-btn" type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}>
|
<input id="gen-data-input" aria-describedby="gen-data-btn" type="{{ widget.type }}" name="{{ widget.name }}"{% if widget.value != None %} value="{{ widget.value|stringformat:'s' }}"{% endif %}{% include "django/forms/widgets/attrs.html" %}>
|
||||||
<div class="input-group-append" onclick="fetchNewIdentifier()">
|
<div class="input-group-append" onclick="fetchNewIdentifier()">
|
||||||
<span id="gen-id-btn" class="btn btn-default" value="{% trans 'Generate new' %}" title="{% trans 'Generate new' %}">{% fa5_icon 'dice' %}</span>
|
<span id="gen-data-btn" class="btn btn-default" value="{% trans 'Generate new' %}" title="{% trans 'Generate new' %}">{% fa5_icon 'dice' %}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
@ -13,7 +13,7 @@
|
|||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(function(data){
|
.then(function(data){
|
||||||
document.getElementById("gen-id-input").value = data["identifier"];
|
document.getElementById("gen-data-input").value = data["gen_data"];
|
||||||
})
|
})
|
||||||
.catch(function(error){
|
.catch(function(error){
|
||||||
console.log(error);
|
console.log(error);
|
||||||
|
@ -7,6 +7,7 @@ Created on: 26.10.21
|
|||||||
"""
|
"""
|
||||||
import datetime
|
import datetime
|
||||||
|
|
||||||
|
from ema.models import Ema
|
||||||
from user.models import User
|
from user.models import User
|
||||||
from django.contrib.auth.models import Group
|
from django.contrib.auth.models import Group
|
||||||
from django.contrib.gis.geos import MultiPolygon, Polygon
|
from django.contrib.gis.geos import MultiPolygon, Polygon
|
||||||
@ -15,7 +16,7 @@ from django.test import TestCase, Client
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from codelist.models import KonovaCode
|
from codelist.models import KonovaCode
|
||||||
from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount
|
from compensation.models import Compensation, CompensationState, CompensationAction, EcoAccount, EcoAccountDeduction
|
||||||
from intervention.models import Legal, Responsibility, Intervention
|
from intervention.models import Legal, Responsibility, Intervention
|
||||||
from konova.management.commands.setup_data import GROUPS_DATA
|
from konova.management.commands.setup_data import GROUPS_DATA
|
||||||
from konova.models import Geometry
|
from konova.models import Geometry
|
||||||
@ -52,6 +53,8 @@ class BaseTestCase(TestCase):
|
|||||||
cls.intervention = cls.create_dummy_intervention()
|
cls.intervention = cls.create_dummy_intervention()
|
||||||
cls.compensation = cls.create_dummy_compensation()
|
cls.compensation = cls.create_dummy_compensation()
|
||||||
cls.eco_account = cls.create_dummy_eco_account()
|
cls.eco_account = cls.create_dummy_eco_account()
|
||||||
|
cls.ema = cls.create_dummy_ema()
|
||||||
|
cls.deduction = cls.create_dummy_deduction()
|
||||||
cls.create_dummy_states()
|
cls.create_dummy_states()
|
||||||
cls.create_dummy_action()
|
cls.create_dummy_action()
|
||||||
cls.codes = cls.create_dummy_codes()
|
cls.codes = cls.create_dummy_codes()
|
||||||
@ -168,6 +171,38 @@ class BaseTestCase(TestCase):
|
|||||||
)
|
)
|
||||||
return eco_account
|
return eco_account
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_dummy_ema(cls):
|
||||||
|
""" Creates an ema which can be used for tests
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Create dummy data
|
||||||
|
# Create log entry
|
||||||
|
action = UserActionLogEntry.get_created_action(cls.superuser)
|
||||||
|
geometry = Geometry.objects.create()
|
||||||
|
# Create responsible data object
|
||||||
|
responsible_data = Responsibility.objects.create()
|
||||||
|
# Finally create main object, holding the other objects
|
||||||
|
ema = Ema.objects.create(
|
||||||
|
identifier="TEST",
|
||||||
|
title="Test_title",
|
||||||
|
responsible=responsible_data,
|
||||||
|
created=action,
|
||||||
|
geometry=geometry,
|
||||||
|
comment="Test",
|
||||||
|
)
|
||||||
|
return ema
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def create_dummy_deduction(cls):
|
||||||
|
return EcoAccountDeduction.objects.create(
|
||||||
|
account=cls.create_dummy_eco_account(),
|
||||||
|
intervention=cls.create_dummy_intervention(),
|
||||||
|
surface=100,
|
||||||
|
)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def create_dummy_states(cls):
|
def create_dummy_states(cls):
|
||||||
""" Creates an intervention which can be used for tests
|
""" Creates an intervention which can be used for tests
|
||||||
|
@ -38,6 +38,7 @@ urlpatterns = [
|
|||||||
path('news/', include("news.urls")),
|
path('news/', include("news.urls")),
|
||||||
path('cl/', include("codelist.urls")),
|
path('cl/', include("codelist.urls")),
|
||||||
path('analysis/', include("analysis.urls")),
|
path('analysis/', include("analysis.urls")),
|
||||||
|
path('api/', include("api.urls")),
|
||||||
|
|
||||||
# Generic deadline routes
|
# Generic deadline routes
|
||||||
path('deadline/<id>/remove', remove_deadline_view, name="deadline-remove"),
|
path('deadline/<id>/remove', remove_deadline_view, name="deadline-remove"),
|
||||||
|
@ -13,6 +13,19 @@ import qrcode.image.svg
|
|||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
|
|
||||||
|
def generate_token() -> str:
|
||||||
|
""" Shortcut for default generating of e.g. API token
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
token (str)
|
||||||
|
"""
|
||||||
|
return generate_random_string(
|
||||||
|
length=64,
|
||||||
|
use_numbers=True,
|
||||||
|
use_letters_lc=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def generate_random_string(length: int, use_numbers: bool = False, use_letters_lc: bool = False, use_letters_uc: bool = False) -> str:
|
def generate_random_string(length: int, use_numbers: bool = False, use_letters_lc: bool = False, use_letters_uc: bool = False) -> str:
|
||||||
"""
|
"""
|
||||||
Generates a random string of variable length
|
Generates a random string of variable length
|
||||||
|
@ -11,7 +11,7 @@ from django.core.mail import send_mail
|
|||||||
from django.template.loader import render_to_string
|
from django.template.loader import render_to_string
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from konova.sub_settings.django_settings import DEFAULT_FROM_EMAIL, EMAIL_REPLY_TO
|
from konova.sub_settings.django_settings import DEFAULT_FROM_EMAIL, EMAIL_REPLY_TO, SUPPORT_MAIL_RECIPIENT
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -181,3 +181,24 @@ class Mailer:
|
|||||||
msg
|
msg
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def send_mail_verify_api_token(self, user):
|
||||||
|
""" Send a mail if a user creates a new token
|
||||||
|
|
||||||
|
Args:
|
||||||
|
user (User): The user, having a new api token
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
context = {
|
||||||
|
"user": user,
|
||||||
|
"EMAIL_REPLY_TO": EMAIL_REPLY_TO,
|
||||||
|
}
|
||||||
|
msg = render_to_string("email/api/verify_token.html", context)
|
||||||
|
user_mail_address = [SUPPORT_MAIL_RECIPIENT]
|
||||||
|
self.send(
|
||||||
|
user_mail_address,
|
||||||
|
_("Request for new API token"),
|
||||||
|
msg
|
||||||
|
)
|
||||||
|
|
||||||
|
Binary file not shown.
@ -20,13 +20,13 @@
|
|||||||
#: konova/filters/mixins.py:385 konova/filters/mixins.py:386
|
#: konova/filters/mixins.py:385 konova/filters/mixins.py:386
|
||||||
#: konova/forms.py:140 konova/forms.py:241 konova/forms.py:312
|
#: konova/forms.py:140 konova/forms.py:241 konova/forms.py:312
|
||||||
#: konova/forms.py:339 konova/forms.py:349 konova/forms.py:362
|
#: konova/forms.py:339 konova/forms.py:349 konova/forms.py:362
|
||||||
#: konova/forms.py:374 konova/forms.py:392 user/forms.py:38
|
#: konova/forms.py:374 konova/forms.py:392 user/forms.py:42
|
||||||
#, fuzzy
|
#, fuzzy
|
||||||
msgid ""
|
msgid ""
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"Project-Id-Version: PACKAGE VERSION\n"
|
"Project-Id-Version: PACKAGE VERSION\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"POT-Creation-Date: 2022-01-20 12:30+0100\n"
|
"POT-Creation-Date: 2022-01-28 16:27+0100\n"
|
||||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||||
@ -317,6 +317,7 @@ msgid "Identifier"
|
|||||||
msgstr "Kennung"
|
msgstr "Kennung"
|
||||||
|
|
||||||
#: compensation/forms/forms.py:35 intervention/forms/forms.py:29
|
#: compensation/forms/forms.py:35 intervention/forms/forms.py:29
|
||||||
|
#: user/forms.py:126
|
||||||
msgid "Generated automatically"
|
msgid "Generated automatically"
|
||||||
msgstr "Automatisch generiert"
|
msgstr "Automatisch generiert"
|
||||||
|
|
||||||
@ -1716,11 +1717,11 @@ msgstr "Kontrolle am"
|
|||||||
msgid "Other"
|
msgid "Other"
|
||||||
msgstr "Sonstige"
|
msgstr "Sonstige"
|
||||||
|
|
||||||
#: konova/sub_settings/django_settings.py:154
|
#: konova/sub_settings/django_settings.py:155
|
||||||
msgid "German"
|
msgid "German"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: konova/sub_settings/django_settings.py:155
|
#: konova/sub_settings/django_settings.py:156
|
||||||
msgid "English"
|
msgid "English"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
@ -1803,6 +1804,10 @@ msgstr "{} - Freigegebene Daten gelöscht"
|
|||||||
msgid "{} - Shared data checked"
|
msgid "{} - Shared data checked"
|
||||||
msgstr "{} - Freigegebene Daten geprüft"
|
msgstr "{} - Freigegebene Daten geprüft"
|
||||||
|
|
||||||
|
#: konova/utils/mailer.py:201 templates/email/api/verify_token.html:4
|
||||||
|
msgid "Request for new API token"
|
||||||
|
msgstr "Anfrage für neuen API Token"
|
||||||
|
|
||||||
#: konova/utils/message_templates.py:11
|
#: konova/utils/message_templates.py:11
|
||||||
msgid "There was an error on this form."
|
msgid "There was an error on this form."
|
||||||
msgstr "Es gab einen Fehler im Formular."
|
msgstr "Es gab einen Fehler im Formular."
|
||||||
@ -1937,6 +1942,33 @@ msgstr ""
|
|||||||
msgid "Something happened. We are working on it!"
|
msgid "Something happened. We are working on it!"
|
||||||
msgstr "Irgendetwas ist passiert. Wir arbeiten daran!"
|
msgstr "Irgendetwas ist passiert. Wir arbeiten daran!"
|
||||||
|
|
||||||
|
#: templates/email/api/verify_token.html:7
|
||||||
|
msgid "Hello support"
|
||||||
|
msgstr "Hallo Support"
|
||||||
|
|
||||||
|
#: templates/email/api/verify_token.html:9
|
||||||
|
msgid "you need to verify the API token for user"
|
||||||
|
msgstr "Sie müssen einen API Token für folgenden Nutzer freischalten"
|
||||||
|
|
||||||
|
#: templates/email/api/verify_token.html:15
|
||||||
|
msgid ""
|
||||||
|
"If unsure, please contact the user. The API token can not be used until you "
|
||||||
|
"activated it in the admin backend."
|
||||||
|
msgstr ""
|
||||||
|
"Falls Sie sich unsicher sind, kontaktieren Sie den Nutzer vorher. Der API "
|
||||||
|
"Token kann so lange nicht verwendet werden, wie er noch nicht von Ihnen im "
|
||||||
|
"Admin Backend aktiviert worden ist."
|
||||||
|
|
||||||
|
#: templates/email/api/verify_token.html:18
|
||||||
|
#: templates/email/checking/shared_data_checked.html:17
|
||||||
|
#: templates/email/deleting/shared_data_deleted.html:17
|
||||||
|
#: templates/email/recording/shared_data_recorded.html:17
|
||||||
|
#: templates/email/recording/shared_data_unrecorded.html:17
|
||||||
|
#: templates/email/sharing/shared_access_given.html:18
|
||||||
|
#: templates/email/sharing/shared_access_removed.html:18
|
||||||
|
msgid "Best regards"
|
||||||
|
msgstr "Beste Grüße"
|
||||||
|
|
||||||
#: templates/email/checking/shared_data_checked.html:4
|
#: templates/email/checking/shared_data_checked.html:4
|
||||||
msgid "Shared data checked"
|
msgid "Shared data checked"
|
||||||
msgstr "Freigegebene Daten geprüft"
|
msgstr "Freigegebene Daten geprüft"
|
||||||
@ -1962,15 +1994,6 @@ msgstr ""
|
|||||||
"Das bedeutet, dass die zuständige Zulassungsbehörde die Korrektheit des "
|
"Das bedeutet, dass die zuständige Zulassungsbehörde die Korrektheit des "
|
||||||
"Datensatzes soeben bestätigt hat."
|
"Datensatzes soeben bestätigt hat."
|
||||||
|
|
||||||
#: templates/email/checking/shared_data_checked.html:17
|
|
||||||
#: templates/email/deleting/shared_data_deleted.html:17
|
|
||||||
#: templates/email/recording/shared_data_recorded.html:17
|
|
||||||
#: templates/email/recording/shared_data_unrecorded.html:17
|
|
||||||
#: templates/email/sharing/shared_access_given.html:18
|
|
||||||
#: templates/email/sharing/shared_access_removed.html:18
|
|
||||||
msgid "Best regards"
|
|
||||||
msgstr "Beste Grüße"
|
|
||||||
|
|
||||||
#: templates/email/deleting/shared_data_deleted.html:4
|
#: templates/email/deleting/shared_data_deleted.html:4
|
||||||
msgid "Shared data deleted"
|
msgid "Shared data deleted"
|
||||||
msgstr "Freigegebene Daten gelöscht"
|
msgstr "Freigegebene Daten gelöscht"
|
||||||
@ -2214,39 +2237,51 @@ msgid ""
|
|||||||
" "
|
" "
|
||||||
msgstr ""
|
msgstr ""
|
||||||
"\n"
|
"\n"
|
||||||
" Diese Daten sind noch nicht veröffentlicht und "
|
" Diese Daten sind noch nicht veröffentlicht und können daher "
|
||||||
"können daher aktuell nicht eingesehen werden. Schauen Sie zu einem späteren "
|
"aktuell nicht eingesehen werden. Schauen Sie zu einem späteren Zeitpunkt "
|
||||||
"Zeitpunkt wieder vorbei. \n"
|
"wieder vorbei. \n"
|
||||||
" "
|
" "
|
||||||
|
|
||||||
#: user/forms.py:23
|
#: user/forms.py:27
|
||||||
msgid "Notifications"
|
msgid "Notifications"
|
||||||
msgstr "Benachrichtigungen"
|
msgstr "Benachrichtigungen"
|
||||||
|
|
||||||
#: user/forms.py:25
|
#: user/forms.py:29
|
||||||
msgid "Select the situations when you want to receive a notification"
|
msgid "Select the situations when you want to receive a notification"
|
||||||
msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?"
|
msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?"
|
||||||
|
|
||||||
#: user/forms.py:37
|
#: user/forms.py:41
|
||||||
msgid "Edit notifications"
|
msgid "Edit notifications"
|
||||||
msgstr "Benachrichtigungen bearbeiten"
|
msgstr "Benachrichtigungen bearbeiten"
|
||||||
|
|
||||||
#: user/forms.py:72 user/templates/user/index.html:9
|
#: user/forms.py:76 user/templates/user/index.html:9
|
||||||
msgid "Username"
|
msgid "Username"
|
||||||
msgstr "Nutzername"
|
msgstr "Nutzername"
|
||||||
|
|
||||||
#: user/forms.py:83
|
#: user/forms.py:87
|
||||||
msgid "Person name"
|
msgid "Person name"
|
||||||
msgstr "Name"
|
msgstr "Name"
|
||||||
|
|
||||||
#: user/forms.py:94 user/templates/user/index.html:17
|
#: user/forms.py:98 user/templates/user/index.html:17
|
||||||
msgid "E-Mail"
|
msgid "E-Mail"
|
||||||
msgstr ""
|
msgstr ""
|
||||||
|
|
||||||
#: user/forms.py:108
|
#: user/forms.py:112
|
||||||
msgid "User contact data"
|
msgid "User contact data"
|
||||||
msgstr "Kontaktdaten"
|
msgstr "Kontaktdaten"
|
||||||
|
|
||||||
|
#: user/forms.py:122
|
||||||
|
msgid "Token"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: user/forms.py:137
|
||||||
|
msgid "Create new token"
|
||||||
|
msgstr "Neuen Token generieren"
|
||||||
|
|
||||||
|
#: user/forms.py:138
|
||||||
|
msgid "A new token needs to be validated by an administrator!"
|
||||||
|
msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!"
|
||||||
|
|
||||||
#: user/models/user_action.py:20
|
#: user/models/user_action.py:20
|
||||||
msgid "Unrecorded"
|
msgid "Unrecorded"
|
||||||
msgstr "Entzeichnet"
|
msgstr "Entzeichnet"
|
||||||
@ -2300,18 +2335,58 @@ msgstr "Benachrichtigungseinstellungen ändern"
|
|||||||
msgid "Notification settings"
|
msgid "Notification settings"
|
||||||
msgstr "Benachrichtigungen"
|
msgstr "Benachrichtigungen"
|
||||||
|
|
||||||
#: user/views.py:29
|
#: user/templates/user/index.html:58
|
||||||
|
msgid "See or edit your API token"
|
||||||
|
msgstr "API token einsehen oder neu generieren"
|
||||||
|
|
||||||
|
#: user/templates/user/index.html:61
|
||||||
|
msgid "API"
|
||||||
|
msgstr ""
|
||||||
|
|
||||||
|
#: user/templates/user/token.html:6
|
||||||
|
msgid "API settings"
|
||||||
|
msgstr "API Einstellungen"
|
||||||
|
|
||||||
|
#: user/templates/user/token.html:10
|
||||||
|
msgid "Current token"
|
||||||
|
msgstr "Aktueller Token"
|
||||||
|
|
||||||
|
#: user/templates/user/token.html:14
|
||||||
|
msgid "Authenticated by admins"
|
||||||
|
msgstr "Von Admin freigeschaltet"
|
||||||
|
|
||||||
|
#: user/templates/user/token.html:18
|
||||||
|
msgid "Token has been verified and can be used"
|
||||||
|
msgstr "Token wurde freigeschaltet und kann verwendet werden"
|
||||||
|
|
||||||
|
#: user/templates/user/token.html:20
|
||||||
|
msgid "Token waiting for verification"
|
||||||
|
msgstr "Token noch nicht freigeschaltet"
|
||||||
|
|
||||||
|
#: user/templates/user/token.html:24
|
||||||
|
msgid "Valid until"
|
||||||
|
msgstr "Läuft ab am"
|
||||||
|
|
||||||
|
#: user/views.py:31
|
||||||
msgid "User settings"
|
msgid "User settings"
|
||||||
msgstr "Einstellungen"
|
msgstr "Einstellungen"
|
||||||
|
|
||||||
#: user/views.py:55
|
#: user/views.py:57
|
||||||
msgid "Notifications edited"
|
msgid "Notifications edited"
|
||||||
msgstr "Benachrichtigungen bearbeitet"
|
msgstr "Benachrichtigungen bearbeitet"
|
||||||
|
|
||||||
#: user/views.py:67
|
#: user/views.py:69
|
||||||
msgid "User notifications"
|
msgid "User notifications"
|
||||||
msgstr "Benachrichtigungen"
|
msgstr "Benachrichtigungen"
|
||||||
|
|
||||||
|
#: user/views.py:92
|
||||||
|
msgid "New token generated. Administrators need to validate."
|
||||||
|
msgstr "Neuer Token generiert. Administratoren sind informiert."
|
||||||
|
|
||||||
|
#: user/views.py:103
|
||||||
|
msgid "User API token"
|
||||||
|
msgstr "API Nutzer Token"
|
||||||
|
|
||||||
#: venv/lib/python3.7/site-packages/bootstrap4/components.py:17
|
#: venv/lib/python3.7/site-packages/bootstrap4/components.py:17
|
||||||
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3
|
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/form_errors.html:3
|
||||||
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/messages.html:4
|
#: venv/lib/python3.7/site-packages/bootstrap4/templates/bootstrap4/messages.html:4
|
||||||
@ -3056,7 +3131,7 @@ msgstr ""
|
|||||||
|
|
||||||
#: venv/lib/python3.7/site-packages/django/forms/fields.py:54
|
#: venv/lib/python3.7/site-packages/django/forms/fields.py:54
|
||||||
msgid "This field is required."
|
msgid "This field is required."
|
||||||
msgstr ""
|
msgstr "Pflichtfeld"
|
||||||
|
|
||||||
#: venv/lib/python3.7/site-packages/django/forms/fields.py:247
|
#: venv/lib/python3.7/site-packages/django/forms/fields.py:247
|
||||||
msgid "Enter a whole number."
|
msgid "Enter a whole number."
|
||||||
|
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 import forms
|
||||||
from django.urls import reverse
|
from django.db import IntegrityError
|
||||||
|
from django.urls import reverse, reverse_lazy
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
from api.models import APIUserToken
|
||||||
|
from intervention.inputs import GenerateInput
|
||||||
from user.models import User
|
from user.models import User
|
||||||
|
|
||||||
from konova.forms import BaseForm, BaseModalForm
|
from konova.forms import BaseForm, BaseModalForm
|
||||||
@ -113,3 +117,46 @@ class UserContactForm(BaseModalForm):
|
|||||||
self.initialize_form_field("mail", self.instance.email)
|
self.initialize_form_field("mail", self.instance.email)
|
||||||
|
|
||||||
|
|
||||||
|
class UserAPITokenForm(BaseForm):
|
||||||
|
token = forms.CharField(
|
||||||
|
label=_("Token"),
|
||||||
|
label_suffix="",
|
||||||
|
max_length=255,
|
||||||
|
required=True,
|
||||||
|
help_text=_("Generated automatically"),
|
||||||
|
widget=GenerateInput(
|
||||||
|
attrs={
|
||||||
|
"class": "form-control",
|
||||||
|
"url": reverse_lazy("api:generate-new-token"),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.form_title = _("Create new token")
|
||||||
|
self.form_caption = _("A new token needs to be validated by an administrator!")
|
||||||
|
|
||||||
|
self.action_url = reverse("user:api-token")
|
||||||
|
self.cancel_redirect = reverse("user:index")
|
||||||
|
|
||||||
|
# Make direct token editing by user impossible. Instead set the proper url for generating a new token
|
||||||
|
self.initialize_form_field("token", None)
|
||||||
|
self.fields["token"].widget.attrs["readonly"] = True
|
||||||
|
|
||||||
|
def save(self):
|
||||||
|
""" Saves the form data
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
api_token (APIUserToken)
|
||||||
|
"""
|
||||||
|
user = self.instance
|
||||||
|
new_token = self.cleaned_data["token"]
|
||||||
|
if user.api_token is not None:
|
||||||
|
user.api_token.delete()
|
||||||
|
new_token = APIUserToken.objects.create(
|
||||||
|
token=new_token
|
||||||
|
)
|
||||||
|
user.api_token = new_token
|
||||||
|
user.save()
|
||||||
|
return new_token
|
||||||
|
@ -9,18 +9,57 @@ from django.contrib.auth.models import AbstractUser
|
|||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
|
||||||
|
from api.models import APIUserToken
|
||||||
|
from konova.settings import ZB_GROUP, DEFAULT_GROUP, ETS_GROUP
|
||||||
from konova.utils.mailer import Mailer
|
from konova.utils.mailer import Mailer
|
||||||
from user.enums import UserNotificationEnum
|
from user.enums import UserNotificationEnum
|
||||||
|
|
||||||
|
|
||||||
class User(AbstractUser):
|
class User(AbstractUser):
|
||||||
notifications = models.ManyToManyField("user.UserNotification", related_name="+", blank=True)
|
notifications = models.ManyToManyField("user.UserNotification", related_name="+", blank=True)
|
||||||
|
api_token = models.OneToOneField(
|
||||||
|
"api.APIUserToken",
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
help_text="The user's API token",
|
||||||
|
on_delete=models.SET_NULL
|
||||||
|
)
|
||||||
|
|
||||||
def is_notification_setting_set(self, notification_enum: UserNotificationEnum):
|
def is_notification_setting_set(self, notification_enum: UserNotificationEnum):
|
||||||
return self.notifications.filter(
|
return self.notifications.filter(
|
||||||
id=notification_enum.value
|
id=notification_enum.value
|
||||||
).exists()
|
).exists()
|
||||||
|
|
||||||
|
def is_zb_user(self):
|
||||||
|
""" Shortcut for checking whether a user is of a special group or not
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool
|
||||||
|
"""
|
||||||
|
return self.groups.filter(
|
||||||
|
name=ZB_GROUP
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
def is_default_user(self):
|
||||||
|
""" Shortcut for checking whether a user is of a special group or not
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool
|
||||||
|
"""
|
||||||
|
return self.groups.filter(
|
||||||
|
name=DEFAULT_GROUP
|
||||||
|
).exists()
|
||||||
|
|
||||||
|
def is_ets_user(self):
|
||||||
|
""" Shortcut for checking whether a user is of a special group or not
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool
|
||||||
|
"""
|
||||||
|
return self.groups.filter(
|
||||||
|
name=ETS_GROUP
|
||||||
|
).exists()
|
||||||
|
|
||||||
def send_mail_shared_access_removed(self, obj_identifier):
|
def send_mail_shared_access_removed(self, obj_identifier):
|
||||||
""" Sends a mail to the user in case of removed shared access
|
""" Sends a mail to the user in case of removed shared access
|
||||||
|
|
||||||
@ -104,3 +143,19 @@ class User(AbstractUser):
|
|||||||
if notification_set:
|
if notification_set:
|
||||||
mailer = Mailer()
|
mailer = Mailer()
|
||||||
mailer.send_mail_shared_data_checked(obj_identifier, self)
|
mailer.send_mail_shared_data_checked(obj_identifier, self)
|
||||||
|
|
||||||
|
def get_API_token(self):
|
||||||
|
""" Getter for an API token
|
||||||
|
|
||||||
|
Creates a new one if none exists, yet.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
token (APIUserToken)
|
||||||
|
"""
|
||||||
|
if self.api_token is None:
|
||||||
|
token = APIUserToken.objects.create()
|
||||||
|
self.api_token = token
|
||||||
|
self.save()
|
||||||
|
else:
|
||||||
|
token = self.api_token
|
||||||
|
return token
|
||||||
|
@ -54,6 +54,14 @@
|
|||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="row mb-2">
|
||||||
|
<a href="{% url 'user:api-token' %}" title="{% trans 'See or edit your API token' %}">
|
||||||
|
<button class="btn btn-default">
|
||||||
|
{% fa5_icon 'code' %}
|
||||||
|
<span>{% trans 'API' %}</span>
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
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 = [
|
urlpatterns = [
|
||||||
path("", index_view, name="index"),
|
path("", index_view, name="index"),
|
||||||
path("notifications/", notifications_view, name="notifications"),
|
path("notifications/", notifications_view, name="notifications"),
|
||||||
|
path("token/api", api_token_view, name="api-token"),
|
||||||
path("contact/<id>", contact_view, name="contact"),
|
path("contact/<id>", contact_view, name="contact"),
|
||||||
|
|
||||||
]
|
]
|
@ -2,14 +2,16 @@ from django.contrib import messages
|
|||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
|
|
||||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||||
|
from konova.utils.mailer import Mailer
|
||||||
|
from konova.utils.message_templates import FORM_INVALID
|
||||||
from user.models import User
|
from user.models import User
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest
|
||||||
from django.shortcuts import render, redirect, get_object_or_404
|
from django.shortcuts import render, redirect, get_object_or_404
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
from konova.decorators import any_group_check
|
from konova.decorators import any_group_check, default_group_required
|
||||||
from user.forms import UserNotificationForm, UserContactForm
|
from user.forms import UserNotificationForm, UserContactForm, UserAPITokenForm
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -70,6 +72,40 @@ def notifications_view(request: HttpRequest):
|
|||||||
return render(request, template, context)
|
return render(request, template, context)
|
||||||
|
|
||||||
|
|
||||||
|
@login_required
|
||||||
|
@default_group_required
|
||||||
|
def api_token_view(request: HttpRequest):
|
||||||
|
""" Handles the request for user api frontend settings
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
template = "user/token.html"
|
||||||
|
user = request.user
|
||||||
|
form = UserAPITokenForm(request.POST or None, instance=user)
|
||||||
|
if request.method == "POST":
|
||||||
|
if form.is_valid():
|
||||||
|
token = form.save()
|
||||||
|
messages.info(request, _("New token generated. Administrators need to validate."))
|
||||||
|
mailer = Mailer()
|
||||||
|
mailer.send_mail_verify_api_token(user)
|
||||||
|
return redirect("user:api-token")
|
||||||
|
else:
|
||||||
|
messages.error(request, FORM_INVALID, extra_tags="danger")
|
||||||
|
elif request.method != "GET":
|
||||||
|
raise NotImplementedError
|
||||||
|
context = {
|
||||||
|
"user": user,
|
||||||
|
"form": form,
|
||||||
|
TAB_TITLE_IDENTIFIER: _("User API token"),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, template, context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def contact_view(request: HttpRequest, id: str):
|
def contact_view(request: HttpRequest, id: str):
|
||||||
""" Renders contact modal view of a users contact data
|
""" Renders contact modal view of a users contact data
|
||||||
|
Loading…
Reference in New Issue
Block a user