Compare commits
112 Commits
Author | SHA1 | Date | |
---|---|---|---|
faf8aed777 | |||
94c498866f | |||
616965c890 | |||
e39c7eb51f | |||
19bd408fbd | |||
7bfe6a37f8 | |||
9b63307f01 | |||
123a470006 | |||
5a0c5285e7 | |||
3a01eaff92 | |||
2af91aa178 | |||
53d0af89ac | |||
7b5c1f0d97 | |||
ef076c0b3b | |||
72a5075f3b | |||
d677ac6b5a | |||
9149e4cbd3 | |||
1c24cbea26 | |||
fa89bbba99 | |||
78eb711057 | |||
416ad8478c | |||
6b28c4ec15 | |||
46a2a4ff46 | |||
90e5cf5b36 | |||
50f46e319c | |||
e2ea087c4e | |||
a6e43b044b | |||
be0d261e81 | |||
62e1b046c3 | |||
669a12410f | |||
dd77e6c16e | |||
33774ce557 | |||
dc3dc99b3d | |||
315f9de958 | |||
0726c15086 | |||
2492a8abe8 | |||
dbc5cba5d7 | |||
c8948ddaea | |||
5039da28aa | |||
4567339570 | |||
768849e646 | |||
ebf10645fc | |||
df241747cf | |||
94e9035e10 | |||
d4d39689cc | |||
2fde3f0fa3 | |||
b62c2e92c9 | |||
1c0fb801e6 | |||
a1acff5e90 | |||
25a92f59aa | |||
884db6f014 | |||
59b7f3c69a | |||
0446d50438 | |||
12f78c85bf | |||
78485a4506 | |||
21a5c84b18 | |||
a93f509d51 | |||
38967da201 | |||
60d749db2d | |||
dff577309e | |||
ea590d0868 | |||
e09c15bd51 | |||
c3019f83fd | |||
93a71a7055 | |||
35b1409359 | |||
c9aeb393b5 | |||
6df46e7642 | |||
fe366bc568 | |||
a9f04a28c1 | |||
8c9f4888dd | |||
5c727b2eaa | |||
76b2a78fe2 | |||
86db08fca0 | |||
fe1dce6440 | |||
a5e6f5a1db | |||
78e9cbab71 | |||
572348f9f1 | |||
8ff3cb9adc | |||
f135008447 | |||
94b7f3ad70 | |||
d69bab36da | |||
fa86cc142f | |||
6523891703 | |||
18f590f4a6 | |||
b441518334 | |||
1a80912960 | |||
04dc7fcd30 | |||
09546212b9 | |||
b1cd7dee40 | |||
c772e1de06 | |||
4332a750d1 | |||
47279dd55d | |||
e2eb0ecbb0 | |||
be5b8457a6 | |||
72f1d80261 | |||
df55c16498 | |||
11cc8b6766 | |||
0b5691f501 | |||
d76a1fc85f | |||
476447c621 | |||
2b94e537ae | |||
c06088a260 | |||
4fc15f6a9d | |||
cf90f9710c | |||
8bcccb4685 | |||
50bd6feb89 | |||
d911f4a3a3 | |||
ab5cdfbcbf | |||
814afb39ae | |||
c2ef5160a3 | |||
e63fb6b8b6 | |||
425ebe54e6 |
48
.env.sample
Normal file
48
.env.sample
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# General
|
||||||
|
SECRET_KEY=CHANGE_ME
|
||||||
|
DEBUG=True
|
||||||
|
ALLOWED_HOSTS=127.0.0.1,localhost,example.org
|
||||||
|
BASE_URL=http://localhost:8002
|
||||||
|
ADMINS=Admin1:mail@example.org,Admin2:mail2@example.org
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_NAME=konova
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=5432
|
||||||
|
|
||||||
|
# Redis (for celery)
|
||||||
|
REDIS_HOST=CHANGE_ME
|
||||||
|
REDIS_PORT=CHANGE_ME
|
||||||
|
|
||||||
|
# E-Mail
|
||||||
|
SMTP_HOST=localhost
|
||||||
|
SMTP_PORT=25
|
||||||
|
REPLY_TO_ADDR=ksp-servicestelle@sgdnord.rlp.de
|
||||||
|
DEFAULT_FROM_EMAIL=service@ksp.de
|
||||||
|
|
||||||
|
# Proxy
|
||||||
|
PROXY=CHANGE_ME
|
||||||
|
MAP_PROXY_HOST_WHITELIST=CHANGE_ME_1,CHANGE_ME_2
|
||||||
|
GEOPORTAL_RLP_USER=CHANGE_ME
|
||||||
|
GEOPORTAL_RLP_PASSWORD=CHANGE_ME
|
||||||
|
|
||||||
|
# Schneider
|
||||||
|
SCHNEIDER_BASE_URL=https://schneider.naturschutz.rlp.de
|
||||||
|
SCHNEIDER_AUTH_TOKEN=CHANGE_ME
|
||||||
|
SCHNEIDER_AUTH_HEADER=auth
|
||||||
|
|
||||||
|
# SSO
|
||||||
|
SSO_SERVER_BASE_URL=https://login.naturschutz.rlp.de
|
||||||
|
OAUTH_CODE_VERIFIER=CHANGE_ME
|
||||||
|
OAUTH_CLIENT_ID=CHANGE_ME
|
||||||
|
OAUTH_CLIENT_SECRET=CHANGE_ME
|
||||||
|
PROPAGATION_SECRET=CHANGE_ME
|
||||||
|
|
||||||
|
# RabbitMQ
|
||||||
|
## For connections to EGON
|
||||||
|
EGON_RABBITMQ_HOST=CHANGE_ME
|
||||||
|
EGON_RABBITMQ_PORT=CHANGE_ME
|
||||||
|
EGON_RABBITMQ_USER=CHANGE_ME
|
||||||
|
EGON_RABBITMQ_PW=CHANGE_ME
|
1
.gitignore
vendored
1
.gitignore
vendored
@ -3,3 +3,4 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
/.coverage
|
/.coverage
|
||||||
/htmlcov/
|
/htmlcov/
|
||||||
|
/.env
|
||||||
|
15
api/admin.py
15
api/admin.py
@ -1,6 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from api.models.token import APIUserToken
|
from api.models.token import APIUserToken, OAuthToken
|
||||||
|
|
||||||
|
|
||||||
class APITokenAdmin(admin.ModelAdmin):
|
class APITokenAdmin(admin.ModelAdmin):
|
||||||
@ -17,4 +17,17 @@ class APITokenAdmin(admin.ModelAdmin):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthTokenAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
"access_token",
|
||||||
|
"refresh_token",
|
||||||
|
"expires_on",
|
||||||
|
]
|
||||||
|
search_fields = [
|
||||||
|
"access_token",
|
||||||
|
"refresh_token",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(APIUserToken, APITokenAdmin)
|
admin.site.register(APIUserToken, APITokenAdmin)
|
||||||
|
admin.site.register(OAuthToken, OAuthTokenAdmin)
|
||||||
|
26
api/migrations/0003_oauthtoken.py
Normal file
26
api/migrations/0003_oauthtoken.py
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.0.4 on 2024-04-30 07:20
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api', '0002_alter_apiusertoken_valid_until'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OAuthToken',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('access_token', models.CharField(db_comment='OAuth access token', max_length=255)),
|
||||||
|
('refresh_token', models.CharField(db_comment='OAuth refresh token', max_length=255)),
|
||||||
|
('expires_on', models.DateTimeField(db_comment='When the token will be expired')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
@ -1,7 +1,14 @@
|
|||||||
|
import json
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import requests
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from konova.models import UuidModel
|
||||||
|
from konova.sub_settings.sso_settings import OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, SSO_SERVER_BASE
|
||||||
from konova.utils.generators import generate_token
|
from konova.utils.generators import generate_token
|
||||||
|
|
||||||
|
|
||||||
@ -44,5 +51,129 @@ class APIUserToken(models.Model):
|
|||||||
if token_obj.valid_until is not None and token_obj.valid_until < _today:
|
if token_obj.valid_until is not None and token_obj.valid_until < _today:
|
||||||
raise PermissionError("Token validity expired")
|
raise PermissionError("Token validity expired")
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise PermissionError("Credentials invalid")
|
raise PermissionError("Token unknown")
|
||||||
return token_obj.user
|
return token_obj.user
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthToken(UuidModel):
|
||||||
|
access_token = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
|
db_comment="OAuth access token"
|
||||||
|
)
|
||||||
|
refresh_token = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
|
db_comment="OAuth refresh token"
|
||||||
|
)
|
||||||
|
expires_on = models.DateTimeField(
|
||||||
|
db_comment="When the token will be expired"
|
||||||
|
)
|
||||||
|
|
||||||
|
ASSUMED_LATENCY = 1000 # assumed latency between creation and receiving of an access token
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.access_token)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_access_token_response(access_token_data: str, received_on):
|
||||||
|
"""
|
||||||
|
Creates an OAuthToken based on retrieved access token data (OAuth2.0 specification)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
access_token_data (str): OAuth2.0 response data
|
||||||
|
received_on (): Timestamp when the response has been received
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
oauth_token = OAuthToken()
|
||||||
|
data = json.loads(access_token_data)
|
||||||
|
|
||||||
|
oauth_token.access_token = data.get("access_token")
|
||||||
|
oauth_token.refresh_token = data.get("refresh_token")
|
||||||
|
|
||||||
|
expires_on = received_on + timedelta(
|
||||||
|
seconds=(data.get("expires_in") + OAuthToken.ASSUMED_LATENCY)
|
||||||
|
)
|
||||||
|
oauth_token.expires_on = expires_on
|
||||||
|
|
||||||
|
return oauth_token
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
url = f"{SSO_SERVER_BASE}o/token/"
|
||||||
|
params = {
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"refresh_token": self.refresh_token,
|
||||||
|
"client_id": OAUTH_CLIENT_ID,
|
||||||
|
"client_secret": OAUTH_CLIENT_SECRET
|
||||||
|
}
|
||||||
|
response = requests.post(
|
||||||
|
url,
|
||||||
|
params
|
||||||
|
)
|
||||||
|
_now = now()
|
||||||
|
is_response_invalid = response.status_code != 200
|
||||||
|
if is_response_invalid:
|
||||||
|
raise RuntimeError(f"Refreshing token not possible: {response.status_code}")
|
||||||
|
|
||||||
|
response_content = response.content.decode("utf-8")
|
||||||
|
response_content = json.loads(response_content)
|
||||||
|
|
||||||
|
access_token = response_content.get("access_token")
|
||||||
|
refresh_token = response_content.get("refresh_token")
|
||||||
|
expires_in = response_content.get("expires")
|
||||||
|
|
||||||
|
self.access_token = access_token
|
||||||
|
self.refresh_token = refresh_token
|
||||||
|
self.expires_in = expires_in
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def update_and_get_user(self):
|
||||||
|
from user.models import User
|
||||||
|
url = f"{SSO_SERVER_BASE}users/oauth/data/"
|
||||||
|
|
||||||
|
access_token = self.access_token
|
||||||
|
response = requests.get(
|
||||||
|
url,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
is_response_code_invalid = response.status_code != 200
|
||||||
|
if is_response_code_invalid:
|
||||||
|
raise RuntimeError(f"OAuth user data fetching unsuccessful: {response.status_code}")
|
||||||
|
|
||||||
|
response_content = response.content.decode("utf-8")
|
||||||
|
response_content = json.loads(response_content)
|
||||||
|
user = User.oauth_update_user(response_content)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
def revoke(self) -> int:
|
||||||
|
""" Revokes the OAuth2 token of the user
|
||||||
|
|
||||||
|
(/o/revoke_token/ indeed removes the corresponding access token on provider side and invalidates the
|
||||||
|
submitted refresh token in one step)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
revocation_status_code (int): HTTP status code for revocation of refresh_token
|
||||||
|
"""
|
||||||
|
revoke_url = f"{SSO_SERVER_BASE}o/revoke_token/"
|
||||||
|
token = self.refresh_token
|
||||||
|
revocation_status_code = requests.post(
|
||||||
|
revoke_url,
|
||||||
|
data={
|
||||||
|
'token': token,
|
||||||
|
'token_type_hint': "refresh_token",
|
||||||
|
},
|
||||||
|
auth=(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET),
|
||||||
|
).status_code
|
||||||
|
|
||||||
|
return revocation_status_code
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ from abc import abstractmethod
|
|||||||
from django.contrib.gis import geos
|
from django.contrib.gis import geos
|
||||||
from django.contrib.gis.geos import GEOSGeometry
|
from django.contrib.gis.geos import GEOSGeometry
|
||||||
from django.core.paginator import Paginator
|
from django.core.paginator import Paginator
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
|
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
|
||||||
from konova.utils.message_templates import DATA_UNSHARED
|
from konova.utils.message_templates import DATA_UNSHARED
|
||||||
@ -32,8 +33,8 @@ class AbstractModelAPISerializer:
|
|||||||
self.lookup = {
|
self.lookup = {
|
||||||
"id": None, # must be set
|
"id": None, # must be set
|
||||||
"deleted__isnull": True,
|
"deleted__isnull": True,
|
||||||
"users__in": [], # must be set
|
|
||||||
}
|
}
|
||||||
|
self.shared_lookup = Q() # must be set, so user or team based share will be used properly
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@ -76,7 +77,11 @@ class AbstractModelAPISerializer:
|
|||||||
else:
|
else:
|
||||||
# Return certain object
|
# Return certain object
|
||||||
self.lookup["id"] = _id
|
self.lookup["id"] = _id
|
||||||
self.lookup["users__in"] = [user]
|
|
||||||
|
self.shared_lookup = Q(
|
||||||
|
Q(users__in=[user]) |
|
||||||
|
Q(teams__in=list(user.shared_teams))
|
||||||
|
)
|
||||||
|
|
||||||
def fetch_and_serialize(self):
|
def fetch_and_serialize(self):
|
||||||
""" Serializes the model entry according to the given lookup data
|
""" Serializes the model entry according to the given lookup data
|
||||||
@ -86,7 +91,13 @@ class AbstractModelAPISerializer:
|
|||||||
Returns:
|
Returns:
|
||||||
serialized_data (dict)
|
serialized_data (dict)
|
||||||
"""
|
"""
|
||||||
entries = self.model.objects.filter(**self.lookup).order_by("id")
|
entries = self.model.objects.filter(
|
||||||
|
**self.lookup
|
||||||
|
).filter(
|
||||||
|
self.shared_lookup
|
||||||
|
).order_by(
|
||||||
|
"id"
|
||||||
|
).distinct()
|
||||||
self.paginator = Paginator(entries, self.rpp)
|
self.paginator = Paginator(entries, self.rpp)
|
||||||
requested_entries = self.paginator.page(self.page_number)
|
requested_entries = self.paginator.page(self.page_number)
|
||||||
|
|
||||||
|
@ -6,6 +6,7 @@ Created on: 24.01.22
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin
|
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin
|
||||||
from compensation.models import Compensation
|
from compensation.models import Compensation
|
||||||
@ -21,8 +22,10 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
|
|||||||
|
|
||||||
def prepare_lookup(self, id, user):
|
def prepare_lookup(self, id, user):
|
||||||
super().prepare_lookup(id, user)
|
super().prepare_lookup(id, user)
|
||||||
del self.lookup["users__in"]
|
self.shared_lookup = Q(
|
||||||
self.lookup["intervention__users__in"] = [user]
|
Q(intervention__users__in=[user]) |
|
||||||
|
Q(intervention__teams__in=user.shared_teams)
|
||||||
|
)
|
||||||
|
|
||||||
def intervention_to_json(self, entry):
|
def intervention_to_json(self, entry):
|
||||||
return {
|
return {
|
||||||
|
@ -6,6 +6,7 @@ Created on: 28.01.22
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from api.utils.serializer.v1.serializer import DeductableAPISerializerV1Mixin, AbstractModelAPISerializerV1
|
from api.utils.serializer.v1.serializer import DeductableAPISerializerV1Mixin, AbstractModelAPISerializerV1
|
||||||
from compensation.models import EcoAccountDeduction, EcoAccount
|
from compensation.models import EcoAccountDeduction, EcoAccount
|
||||||
@ -28,9 +29,11 @@ class DeductionAPISerializerV1(AbstractModelAPISerializerV1,
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
super().prepare_lookup(_id, user)
|
super().prepare_lookup(_id, user)
|
||||||
del self.lookup["users__in"]
|
|
||||||
del self.lookup["deleted__isnull"]
|
del self.lookup["deleted__isnull"]
|
||||||
self.lookup["intervention__users__in"] = [user]
|
self.shared_lookup = Q(
|
||||||
|
Q(intervention__users__in=[user]) |
|
||||||
|
Q(intervention__teams__in=user.shared_teams)
|
||||||
|
)
|
||||||
|
|
||||||
def _model_to_geo_json(self, entry):
|
def _model_to_geo_json(self, entry):
|
||||||
""" Adds the basic data
|
""" Adds the basic data
|
||||||
|
@ -16,7 +16,8 @@ from api.utils.serializer.serializer import AbstractModelAPISerializer
|
|||||||
from codelist.models import KonovaCode
|
from codelist.models import KonovaCode
|
||||||
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \
|
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \
|
||||||
CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \
|
CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \
|
||||||
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_HANDLER_ID
|
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_HANDLER_ID, \
|
||||||
|
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
from compensation.models import CompensationAction, UnitChoices, CompensationState
|
from compensation.models import CompensationAction, UnitChoices, CompensationState
|
||||||
from intervention.models import Responsibility, Legal, Handler
|
from intervention.models import Responsibility, Legal, Handler
|
||||||
from konova.models import Deadline, DeadlineType
|
from konova.models import Deadline, DeadlineType
|
||||||
@ -347,7 +348,7 @@ class AbstractCompensationAPISerializerV1Mixin:
|
|||||||
try:
|
try:
|
||||||
biotope_type = entry["biotope"]
|
biotope_type = entry["biotope"]
|
||||||
biotope_details = [
|
biotope_details = [
|
||||||
self._konova_code_from_json(e, CODELIST_BIOTOPES_EXTRA_CODES_ID) for e in entry["biotope_details"]
|
self._konova_code_from_json(e, CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID) for e in entry["biotope_details"]
|
||||||
]
|
]
|
||||||
surface = float(entry["surface"])
|
surface = float(entry["surface"])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -23,11 +23,6 @@ class AbstractAPIViewV1(AbstractAPIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
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)
|
super().__init__(*args, **kwargs)
|
||||||
self.serializer = self.serializer()
|
self.serializer = self.serializer()
|
||||||
|
|
||||||
|
@ -50,13 +50,18 @@ class AbstractAPIView(View):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
# Fetch the proper user from the given request header token
|
# Fetch the proper user from the given request header token
|
||||||
ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
|
token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
|
||||||
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
|
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
|
||||||
token_user = APIUserToken.get_user_from_token(ksp_token)
|
|
||||||
|
|
||||||
if ksp_user != token_user.username:
|
if not token and not ksp_user:
|
||||||
|
bearer_token = request.headers.get("authorization", None)
|
||||||
|
if not bearer_token:
|
||||||
|
raise PermissionError("No token provided")
|
||||||
|
token = bearer_token.split(" ")[1]
|
||||||
|
|
||||||
|
token_user = APIUserToken.get_user_from_token(token)
|
||||||
|
if ksp_user and ksp_user != token_user.username:
|
||||||
raise PermissionError(f"Invalid token for {ksp_user}")
|
raise PermissionError(f"Invalid token for {ksp_user}")
|
||||||
else:
|
|
||||||
self.user = token_user
|
self.user = token_user
|
||||||
|
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
|
@ -9,7 +9,8 @@ import collections
|
|||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
|
from codelist.settings import CODELIST_BIOTOPES_ID, \
|
||||||
|
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
from codelist.autocomplete.base import KonovaCodeAutocomplete
|
from codelist.autocomplete.base import KonovaCodeAutocomplete
|
||||||
from konova.utils.message_templates import UNGROUPED
|
from konova.utils.message_templates import UNGROUPED
|
||||||
|
|
||||||
@ -84,11 +85,11 @@ class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
|
|||||||
Due to limitations of the django dal package, we need to subclass for each code list
|
Due to limitations of the django dal package, we need to subclass for each code list
|
||||||
"""
|
"""
|
||||||
group_by_related = "parent"
|
group_by_related = "parent"
|
||||||
related_field_name = "long_name"
|
related_field_name = "short_name"
|
||||||
paginate_by = 200
|
paginate_by = 200
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.c = CODELIST_BIOTOPES_EXTRA_CODES_ID
|
self.c = CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def order_by(self, qs):
|
def order_by(self, qs):
|
||||||
@ -103,8 +104,11 @@ class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
|
|||||||
qs (QuerySet): The ordered queryset
|
qs (QuerySet): The ordered queryset
|
||||||
"""
|
"""
|
||||||
return qs.order_by(
|
return qs.order_by(
|
||||||
"long_name",
|
"short_name",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_result_label(self, result):
|
def get_result_label(self, result):
|
||||||
return f"{result.long_name} ({result.short_name})"
|
return f"{result.long_name} ({result.short_name})"
|
||||||
|
|
||||||
|
def get_selected_result_label(self, result):
|
||||||
|
return f"{result.parent.short_name} > {result.long_name} ({result.short_name})"
|
@ -13,7 +13,7 @@ from codelist.settings import CODELIST_INTERVENTION_HANDLER_ID, CODELIST_CONSERV
|
|||||||
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, CODELIST_HANDLER_ID, \
|
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, CODELIST_HANDLER_ID, \
|
||||||
CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID, \
|
CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID, \
|
||||||
CODELIST_BASE_URL, CODELIST_PROCESS_TYPE_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
|
CODELIST_BASE_URL, CODELIST_PROCESS_TYPE_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
|
||||||
CODELIST_COMPENSATION_ACTION_DETAIL_ID
|
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
from konova.management.commands.setup import BaseKonovaCommand
|
from konova.management.commands.setup import BaseKonovaCommand
|
||||||
from konova.settings import PROXIES
|
from konova.settings import PROXIES
|
||||||
|
|
||||||
@ -34,6 +34,7 @@ class Command(BaseKonovaCommand):
|
|||||||
CODELIST_REGISTRATION_OFFICE_ID,
|
CODELIST_REGISTRATION_OFFICE_ID,
|
||||||
CODELIST_BIOTOPES_ID,
|
CODELIST_BIOTOPES_ID,
|
||||||
CODELIST_BIOTOPES_EXTRA_CODES_ID,
|
CODELIST_BIOTOPES_EXTRA_CODES_ID,
|
||||||
|
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID,
|
||||||
CODELIST_LAW_ID,
|
CODELIST_LAW_ID,
|
||||||
CODELIST_HANDLER_ID,
|
CODELIST_HANDLER_ID,
|
||||||
CODELIST_COMPENSATION_ACTION_ID,
|
CODELIST_COMPENSATION_ACTION_ID,
|
||||||
@ -55,7 +56,7 @@ class Command(BaseKonovaCommand):
|
|||||||
content = result.content.decode("utf-8")
|
content = result.content.decode("utf-8")
|
||||||
root = etree.fromstring(content)
|
root = etree.fromstring(content)
|
||||||
items = root.findall("item")
|
items = root.findall("item")
|
||||||
self._write_warning("Found {} codes. Process now...".format(len(items)))
|
self._write_warning(" Found {} codes. Process now...".format(len(items)))
|
||||||
|
|
||||||
code_list = KonovaCodeList.objects.get_or_create(
|
code_list = KonovaCodeList.objects.get_or_create(
|
||||||
id=list_id,
|
id=list_id,
|
||||||
@ -81,8 +82,8 @@ class Command(BaseKonovaCommand):
|
|||||||
atom_id = element.find("atomid").text
|
atom_id = element.find("atomid").text
|
||||||
selectable = element.find("selectable").text.lower()
|
selectable = element.find("selectable").text.lower()
|
||||||
selectable = bool_map.get(selectable, False)
|
selectable = bool_map.get(selectable, False)
|
||||||
short_name = element.find("shortname").text
|
short_name = element.find("shortname").text or ""
|
||||||
long_name = element.find("longname").text
|
long_name = element.find("longname").text or ""
|
||||||
is_archived = bool_map.get((element.find("archive").text.lower()), False)
|
is_archived = bool_map.get((element.find("archive").text.lower()), False)
|
||||||
|
|
||||||
code = KonovaCode.objects.get_or_create(
|
code = KonovaCode.objects.get_or_create(
|
||||||
|
60
codelist/migrations/0002_migrate_975_to_288.py
Normal file
60
codelist/migrations/0002_migrate_975_to_288.py
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
# Generated by Django 5.0.7 on 2024-08-06 13:40
|
||||||
|
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from codelist.settings import CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_975_to_288(apps, schema_editor):
|
||||||
|
KonovaCodeList = apps.get_model('codelist', 'KonovaCodeList')
|
||||||
|
CompensationState = apps.get_model('compensation', 'CompensationState')
|
||||||
|
|
||||||
|
try:
|
||||||
|
list_288 = KonovaCodeList.objects.get(
|
||||||
|
id=CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
|
).codes.all()
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise AssertionError("KonovaCodeList 288 does not exist. Did you run 'update_codelist' before migrating?")
|
||||||
|
|
||||||
|
states_with_extra_code = CompensationState.objects.filter(
|
||||||
|
~Q(biotope_type_details=None)
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"... Found {states_with_extra_code.count()} biotope state entries")
|
||||||
|
for state in states_with_extra_code:
|
||||||
|
extra_codes_975 = state.biotope_type_details.filter(
|
||||||
|
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID]
|
||||||
|
)
|
||||||
|
count_extra_codes_975 = extra_codes_975.count()
|
||||||
|
if count_extra_codes_975 > 0:
|
||||||
|
print(f" --> Found {count_extra_codes_975} codes from list 975 in biotope entry {state.id}")
|
||||||
|
extra_codes_288 = []
|
||||||
|
for extra_code_975 in extra_codes_975:
|
||||||
|
atom_id = extra_code_975.atom_id
|
||||||
|
codes_from_288 = list_288.filter(
|
||||||
|
atom_id=atom_id,
|
||||||
|
)
|
||||||
|
extra_codes_288 += codes_from_288
|
||||||
|
|
||||||
|
state.biotope_type_details.set(extra_codes_288)
|
||||||
|
print(" --> Migrated to list 288 for all biotope entries")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('codelist', '0001_initial'),
|
||||||
|
('compensation', '0003_auto_20220202_0846'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# If migration of codelist is not necessary, this variable can shut down the logic whilst not disturbing the
|
||||||
|
# migration history
|
||||||
|
EXECUTE_CODELIST_MIGRATION = True
|
||||||
|
|
||||||
|
operations = []
|
||||||
|
|
||||||
|
if EXECUTE_CODELIST_MIGRATION:
|
||||||
|
operations.append(migrations.RunPython(migrate_975_to_288))
|
||||||
|
|
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.0.8 on 2024-08-26 16:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('codelist', '0002_migrate_975_to_288'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='konovacode',
|
||||||
|
name='long_name',
|
||||||
|
field=models.CharField(blank=True, default="", max_length=1000),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='konovacode',
|
||||||
|
name='short_name',
|
||||||
|
field=models.CharField(blank=True, default="", help_text='Short version of long name', max_length=500),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
@ -25,13 +25,11 @@ class KonovaCode(models.Model):
|
|||||||
)
|
)
|
||||||
short_name = models.CharField(
|
short_name = models.CharField(
|
||||||
max_length=500,
|
max_length=500,
|
||||||
null=True,
|
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Short version of long name",
|
help_text="Short version of long name",
|
||||||
)
|
)
|
||||||
long_name = models.CharField(
|
long_name = models.CharField(
|
||||||
max_length=1000,
|
max_length=1000,
|
||||||
null=True,
|
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="",
|
help_text="",
|
||||||
)
|
)
|
||||||
@ -50,12 +48,28 @@ class KonovaCode(models.Model):
|
|||||||
|
|
||||||
def __str__(self, with_parent: bool = True):
|
def __str__(self, with_parent: bool = True):
|
||||||
ret_val = ""
|
ret_val = ""
|
||||||
if self.parent and self.parent.long_name and with_parent:
|
|
||||||
|
long_name = self.long_name
|
||||||
|
short_name = self.short_name
|
||||||
|
|
||||||
|
both_names_exist = long_name is not None and short_name is not None
|
||||||
|
|
||||||
|
if both_names_exist:
|
||||||
|
if with_parent and self.parent:
|
||||||
|
parent_short_name_exists = self.parent.short_name is not None
|
||||||
|
parent_long_name_exists = self.parent.long_name is not None
|
||||||
|
if parent_long_name_exists:
|
||||||
ret_val += self.parent.long_name + " > "
|
ret_val += self.parent.long_name + " > "
|
||||||
ret_val += self.long_name
|
elif parent_short_name_exists:
|
||||||
if self.short_name and self.short_name != self.long_name:
|
ret_val += self.parent.short_name + " > "
|
||||||
# Only add short name, if we won't have stupid repition like 'thing a (thing a)' due to misused long-short names
|
|
||||||
ret_val += f" ({self.short_name})"
|
ret_val += long_name
|
||||||
|
|
||||||
|
if short_name and short_name != long_name:
|
||||||
|
ret_val += f" ({short_name})"
|
||||||
|
else:
|
||||||
|
ret_val += str(long_name or short_name)
|
||||||
|
|
||||||
return ret_val
|
return ret_val
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@ -75,7 +89,8 @@ class KonovaCode(models.Model):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
children = KonovaCode.objects.filter(
|
children = KonovaCode.objects.filter(
|
||||||
parent=self
|
parent=self,
|
||||||
|
is_archived=False,
|
||||||
).order_by(
|
).order_by(
|
||||||
order_by
|
order_by
|
||||||
)
|
)
|
||||||
|
@ -15,7 +15,8 @@ CODELIST_CONSERVATION_OFFICE_ID = 907 # CLNaturschutzbehörden
|
|||||||
CODELIST_REGISTRATION_OFFICE_ID = 1053 # CLZulassungsbehörden
|
CODELIST_REGISTRATION_OFFICE_ID = 1053 # CLZulassungsbehörden
|
||||||
CODELIST_BIOTOPES_ID = 654 # CL_Biotoptypen
|
CODELIST_BIOTOPES_ID = 654 # CL_Biotoptypen
|
||||||
CODELIST_AFTER_STATE_BIOTOPES_ID = 974 # CL-KSP_ZielBiotoptypen - USAGE HAS BEEN DROPPED IN 2022 IN FAVOR OF 654
|
CODELIST_AFTER_STATE_BIOTOPES_ID = 974 # CL-KSP_ZielBiotoptypen - USAGE HAS BEEN DROPPED IN 2022 IN FAVOR OF 654
|
||||||
CODELIST_BIOTOPES_EXTRA_CODES_ID = 975 # CLZusatzbezeichnung
|
CODELIST_BIOTOPES_EXTRA_CODES_ID = 975 # CLZusatzbezeichnung - Subset of 288. Migration usage 975->288 in 08/2024
|
||||||
|
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID = 288 # CLBiotoptypZusatzcode
|
||||||
CODELIST_LAW_ID = 1048 # CLVerfahrensrecht
|
CODELIST_LAW_ID = 1048 # CLVerfahrensrecht
|
||||||
CODELIST_PROCESS_TYPE_ID = 44382 # CLVerfahrenstyp
|
CODELIST_PROCESS_TYPE_ID = 44382 # CLVerfahrenstyp
|
||||||
|
|
||||||
|
@ -148,7 +148,7 @@ class CompensationActionAdmin(admin.ModelAdmin):
|
|||||||
|
|
||||||
admin.site.register(Compensation, CompensationAdmin)
|
admin.site.register(Compensation, CompensationAdmin)
|
||||||
admin.site.register(EcoAccount, EcoAccountAdmin)
|
admin.site.register(EcoAccount, EcoAccountAdmin)
|
||||||
admin.site.register(EcoAccountDeduction, EcoAccountDeductionAdmin)
|
#admin.site.register(EcoAccountDeduction, EcoAccountDeductionAdmin)
|
||||||
|
|
||||||
# For a more cleaner admin interface these rarely used admin views are not important for deployment
|
# For a more cleaner admin interface these rarely used admin views are not important for deployment
|
||||||
#admin.site.register(Payment, PaymentAdmin)
|
#admin.site.register(Payment, PaymentAdmin)
|
||||||
|
@ -213,7 +213,6 @@ class EditCompensationForm(NewCompensationForm):
|
|||||||
action = UserActionLogEntry.get_edited_action(user)
|
action = UserActionLogEntry.get_edited_action(user)
|
||||||
|
|
||||||
# Fetch data from cleaned POST values
|
# Fetch data from cleaned POST values
|
||||||
identifier = self.cleaned_data.get("identifier", None)
|
|
||||||
title = self.cleaned_data.get("title", None)
|
title = self.cleaned_data.get("title", None)
|
||||||
intervention = self.cleaned_data.get("intervention", None)
|
intervention = self.cleaned_data.get("intervention", None)
|
||||||
is_cef = self.cleaned_data.get("is_cef", None)
|
is_cef = self.cleaned_data.get("is_cef", None)
|
||||||
@ -221,7 +220,6 @@ class EditCompensationForm(NewCompensationForm):
|
|||||||
is_pik = self.cleaned_data.get("is_pik", None)
|
is_pik = self.cleaned_data.get("is_pik", None)
|
||||||
comment = self.cleaned_data.get("comment", None)
|
comment = self.cleaned_data.get("comment", None)
|
||||||
|
|
||||||
self.instance.identifier = identifier
|
|
||||||
self.instance.title = title
|
self.instance.title = title
|
||||||
self.instance.intervention = intervention
|
self.instance.intervention = intervention
|
||||||
self.instance.is_cef = is_cef
|
self.instance.is_cef = is_cef
|
||||||
|
@ -192,7 +192,6 @@ class EditEcoAccountForm(NewEcoAccountForm):
|
|||||||
def save(self, user: User, geom_form: SimpleGeomForm):
|
def save(self, user: User, geom_form: SimpleGeomForm):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# Fetch data from cleaned POST values
|
# Fetch data from cleaned POST values
|
||||||
identifier = self.cleaned_data.get("identifier", None)
|
|
||||||
title = self.cleaned_data.get("title", None)
|
title = self.cleaned_data.get("title", None)
|
||||||
registration_date = self.cleaned_data.get("registration_date", None)
|
registration_date = self.cleaned_data.get("registration_date", None)
|
||||||
handler_type = self.cleaned_data.get("handler_type", None)
|
handler_type = self.cleaned_data.get("handler_type", None)
|
||||||
@ -219,7 +218,6 @@ class EditEcoAccountForm(NewEcoAccountForm):
|
|||||||
self.instance.legal.save()
|
self.instance.legal.save()
|
||||||
|
|
||||||
# Update main oject data
|
# Update main oject data
|
||||||
self.instance.identifier = identifier
|
|
||||||
self.instance.title = title
|
self.instance.title = title
|
||||||
self.instance.deductable_surface = surface
|
self.instance.deductable_surface = surface
|
||||||
self.instance.comment = comment
|
self.instance.comment = comment
|
||||||
|
@ -14,7 +14,8 @@ from django.shortcuts import render
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from codelist.models import KonovaCode
|
from codelist.models import KonovaCode
|
||||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
|
from codelist.settings import CODELIST_BIOTOPES_ID, \
|
||||||
|
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
from intervention.inputs import CompensationStateTreeRadioSelect
|
from intervention.inputs import CompensationStateTreeRadioSelect
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
from konova.forms.modals import RemoveModalForm, BaseModalForm
|
from konova.forms.modals import RemoveModalForm, BaseModalForm
|
||||||
@ -43,7 +44,7 @@ class NewCompensationStateModalForm(BaseModalForm):
|
|||||||
queryset=KonovaCode.objects.filter(
|
queryset=KonovaCode.objects.filter(
|
||||||
is_archived=False,
|
is_archived=False,
|
||||||
is_leaf=True,
|
is_leaf=True,
|
||||||
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID],
|
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID],
|
||||||
),
|
),
|
||||||
widget=autocomplete.ModelSelect2Multiple(
|
widget=autocomplete.ModelSelect2Multiple(
|
||||||
url="codelist:biotope-extra-type-autocomplete",
|
url="codelist:biotope-extra-type-autocomplete",
|
||||||
|
@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.0.8 on 2024-08-26 16:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('codelist', '0003_alter_konovacode_long_name_and_more'),
|
||||||
|
('compensation', '0015_alter_compensation_after_states_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='compensationstate',
|
||||||
|
name='biotope_type_details',
|
||||||
|
field=models.ManyToManyField(blank=True, limit_choices_to={'code_lists__in': [288], 'is_archived': False, 'is_selectable': True}, related_name='+', to='codelist.konovacode'),
|
||||||
|
),
|
||||||
|
]
|
@ -315,7 +315,6 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
|
|||||||
def get_detail_url_absolute(self):
|
def get_detail_url_absolute(self):
|
||||||
return BASE_URL + self.get_detail_url()
|
return BASE_URL + self.get_detail_url()
|
||||||
|
|
||||||
|
|
||||||
def save(self, *args, **kwargs):
|
def save(self, *args, **kwargs):
|
||||||
if self.identifier is None or len(self.identifier) == 0:
|
if self.identifier is None or len(self.identifier) == 0:
|
||||||
# Create new identifier is none was given
|
# Create new identifier is none was given
|
||||||
|
@ -6,10 +6,10 @@ Created on: 16.11.21
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from codelist.models import KonovaCode
|
from codelist.models import KonovaCode
|
||||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
|
from codelist.settings import CODELIST_BIOTOPES_ID, \
|
||||||
|
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
from compensation.managers import CompensationStateManager
|
from compensation.managers import CompensationStateManager
|
||||||
from konova.models import UuidModel
|
from konova.models import UuidModel
|
||||||
|
|
||||||
@ -34,7 +34,7 @@ class CompensationState(UuidModel):
|
|||||||
KonovaCode,
|
KonovaCode,
|
||||||
blank=True,
|
blank=True,
|
||||||
limit_choices_to={
|
limit_choices_to={
|
||||||
"code_lists__in": [CODELIST_BIOTOPES_EXTRA_CODES_ID],
|
"code_lists__in": [CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID],
|
||||||
"is_selectable": True,
|
"is_selectable": True,
|
||||||
"is_archived": False,
|
"is_archived": False,
|
||||||
},
|
},
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-action' obj.id %}" title="{% trans 'Add new action' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-action' obj.id %}" title="{% trans 'Add new action' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'seedling' %}
|
{% fa5_icon 'seedling' %}
|
||||||
@ -34,7 +34,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@ -52,7 +52,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for detail in action.action_type_details.all %}
|
{% for detail in action.action_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -64,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
|
<button data-form-url="{% url 'compensation:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
{% fa5_icon 'file-alt' %}
|
{% fa5_icon 'file-alt' %}
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:resubmission-create' obj.id %}">
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:resubmission-create' obj.id %}">
|
||||||
{% fa5_icon 'bell' %}
|
{% fa5_icon 'bell' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'calendar-check' %}
|
{% fa5_icon 'calendar-check' %}
|
||||||
@ -38,7 +38,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@ -60,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
|
<button data-form-url="{% url 'compensation:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'file' %}
|
{% fa5_icon 'file' %}
|
||||||
@ -33,7 +33,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@ -59,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
<button data-form-url="{% url 'compensation:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'layer-group' %}
|
{% fa5_icon 'layer-group' %}
|
||||||
@ -35,7 +35,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Surface' %}
|
{% trans 'Surface' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@ -51,14 +51,14 @@
|
|||||||
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
<br>
|
<br>
|
||||||
{% for detail in state.biotope_type_details.all %}
|
{% for detail in state.biotope_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'layer-group' %}
|
{% fa5_icon 'layer-group' %}
|
||||||
@ -35,7 +35,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Surface' %}
|
{% trans 'Surface' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@ -51,14 +51,14 @@
|
|||||||
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
<br>
|
<br>
|
||||||
{% for detail in state.biotope_type_details.all %}
|
{% for detail in state.biotope_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -123,7 +123,7 @@
|
|||||||
{% include 'user/includes/team_data_modal_button.html' %}
|
{% include 'user/includes/team_data_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<hr>
|
<hr>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
{% for user in obj.intervention.shared_users %}
|
{% for user in obj.intervention.shared_users %}
|
||||||
{% include 'user/includes/contact_modal_button.html' %}
|
{% include 'user/includes/contact_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-action' obj.id %}" title="{% trans 'Add new action' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-action' obj.id %}" title="{% trans 'Add new action' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'seedling' %}
|
{% fa5_icon 'seedling' %}
|
||||||
@ -33,7 +33,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@ -51,7 +51,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for detail in action.action_type_details.all %}
|
{% for detail in action.action_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -63,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:acc:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
|
<button data-form-url="{% url 'compensation:acc:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
{% fa5_icon 'file-alt' %}
|
{% fa5_icon 'file-alt' %}
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:acc:resubmission-create' obj.id %}">
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:acc:resubmission-create' obj.id %}">
|
||||||
{% fa5_icon 'bell' %}
|
{% fa5_icon 'bell' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'calendar-check' %}
|
{% fa5_icon 'calendar-check' %}
|
||||||
@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:acc:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
|
<button data-form-url="{% url 'compensation:acc:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -61,7 +61,7 @@
|
|||||||
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
|
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
|
||||||
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
|
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access or is_default_member and user in deduction.intervention.shared_users %}
|
{% if is_default_member and is_entry_shared or is_default_member and user in deduction.intervention.shared_users %}
|
||||||
<button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
|
<button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'file' %}
|
{% fa5_icon 'file' %}
|
||||||
@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:acc:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
<button data-form-url="{% url 'compensation:acc:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'layer-group' %}
|
{% fa5_icon 'layer-group' %}
|
||||||
@ -35,7 +35,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Surface' %}
|
{% trans 'Surface' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@ -51,14 +51,14 @@
|
|||||||
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
<br>
|
<br>
|
||||||
{% for detail in state.biotope_type_details.all %}
|
{% for detail in state.biotope_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'layer-group' %}
|
{% fa5_icon 'layer-group' %}
|
||||||
@ -35,7 +35,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Surface' %}
|
{% trans 'Surface' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@ -51,14 +51,14 @@
|
|||||||
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
<br>
|
<br>
|
||||||
{% for detail in state.biotope_type_details.all %}
|
{% for detail in state.biotope_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -101,7 +101,7 @@
|
|||||||
{% include 'user/includes/team_data_modal_button.html' %}
|
{% include 'user/includes/team_data_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<hr>
|
<hr>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
{% for user in obj.users.all %}
|
{% for user in obj.users.all %}
|
||||||
{% include 'user/includes/contact_modal_button.html' %}
|
{% include 'user/includes/contact_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -125,10 +125,16 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
self.compensation = self.fill_out_compensation(self.compensation)
|
self.compensation = self.fill_out_compensation(self.compensation)
|
||||||
pre_edit_log_count = self.compensation.log.count()
|
pre_edit_log_count = self.compensation.log.count()
|
||||||
|
|
||||||
|
self.assertTrue(self.compensation.is_shared_with(self.superuser))
|
||||||
|
|
||||||
|
old_identifier = self.compensation.identifier
|
||||||
new_title = self.create_dummy_string()
|
new_title = self.create_dummy_string()
|
||||||
new_identifier = self.create_dummy_string()
|
new_identifier = self.create_dummy_string()
|
||||||
new_comment = self.create_dummy_string()
|
new_comment = self.create_dummy_string()
|
||||||
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
|
new_geometry = MultiPolygon(
|
||||||
|
self.compensation.geometry.geom.buffer(10),
|
||||||
|
srid=self.compensation.geometry.geom.srid
|
||||||
|
) # Create a geometry which differs from the stored one
|
||||||
geojson = self.create_geojson(new_geometry)
|
geojson = self.create_geojson(new_geometry)
|
||||||
|
|
||||||
check_on_elements = {
|
check_on_elements = {
|
||||||
@ -151,19 +157,21 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
|
|
||||||
check_on_elements = {
|
check_on_elements = {
|
||||||
self.compensation.title: new_title,
|
self.compensation.title: new_title,
|
||||||
self.compensation.identifier: new_identifier,
|
|
||||||
self.compensation.comment: new_comment,
|
self.compensation.comment: new_comment,
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v in check_on_elements.items():
|
for k, v in check_on_elements.items():
|
||||||
self.assertEqual(k, v)
|
self.assertEqual(k, v)
|
||||||
|
|
||||||
self.assert_equal_geometries(self.compensation.geometry.geom, new_geometry)
|
# Expect identifier to not be editable
|
||||||
|
self.assertEqual(self.compensation.identifier, old_identifier, msg="Identifier was editable!")
|
||||||
|
|
||||||
# Expect logs to be set
|
# Expect logs to be set
|
||||||
self.assertEqual(pre_edit_log_count + 1, self.compensation.log.count())
|
self.assertEqual(pre_edit_log_count + 1, self.compensation.log.count())
|
||||||
self.assertEqual(self.compensation.log.first().action, UserAction.EDITED)
|
self.assertEqual(self.compensation.log.first().action, UserAction.EDITED)
|
||||||
|
|
||||||
|
self.assert_equal_geometries(self.compensation.geometry.geom, new_geometry)
|
||||||
|
|
||||||
def test_checkability(self):
|
def test_checkability(self):
|
||||||
"""
|
"""
|
||||||
This tests if the checkability of the compensation (which is defined by the linked intervention's checked
|
This tests if the checkability of the compensation (which is defined by the linked intervention's checked
|
||||||
|
@ -82,6 +82,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
|
url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
|
||||||
pre_edit_log_count = self.eco_account.log.count()
|
pre_edit_log_count = self.eco_account.log.count()
|
||||||
|
|
||||||
|
old_identifier = self.eco_account.identifier
|
||||||
new_title = self.create_dummy_string()
|
new_title = self.create_dummy_string()
|
||||||
new_identifier = self.create_dummy_string()
|
new_identifier = self.create_dummy_string()
|
||||||
new_comment = self.create_dummy_string()
|
new_comment = self.create_dummy_string()
|
||||||
@ -114,7 +115,6 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
|
|
||||||
check_on_elements = {
|
check_on_elements = {
|
||||||
self.eco_account.title: new_title,
|
self.eco_account.title: new_title,
|
||||||
self.eco_account.identifier: new_identifier,
|
|
||||||
self.eco_account.deductable_surface: test_deductable_surface,
|
self.eco_account.deductable_surface: test_deductable_surface,
|
||||||
self.eco_account.deductable_rest: test_deductable_surface - deductions_surface,
|
self.eco_account.deductable_rest: test_deductable_surface - deductions_surface,
|
||||||
self.eco_account.comment: new_comment,
|
self.eco_account.comment: new_comment,
|
||||||
@ -123,6 +123,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
for k, v in check_on_elements.items():
|
for k, v in check_on_elements.items():
|
||||||
self.assertEqual(k, v)
|
self.assertEqual(k, v)
|
||||||
|
|
||||||
|
self.assertEqual(self.eco_account.identifier, old_identifier)
|
||||||
self.assert_equal_geometries(self.eco_account.geometry.geom, new_geometry)
|
self.assert_equal_geometries(self.eco_account.geometry.geom, new_geometry)
|
||||||
|
|
||||||
# Expect logs to be set
|
# Expect logs to be set
|
||||||
|
@ -19,7 +19,8 @@ from compensation.models import Compensation
|
|||||||
from compensation.tables.compensation import CompensationTable
|
from compensation.tables.compensation import CompensationTable
|
||||||
from intervention.models import Intervention
|
from intervention.models import Intervention
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal
|
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
|
||||||
|
uuid_required
|
||||||
from konova.forms import SimpleGeomForm
|
from konova.forms import SimpleGeomForm
|
||||||
from konova.forms.modals import RemoveModalForm
|
from konova.forms.modals import RemoveModalForm
|
||||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
||||||
@ -200,6 +201,7 @@ def edit_view(request: HttpRequest, id: str):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@any_group_check
|
@any_group_check
|
||||||
|
@uuid_required
|
||||||
def detail_view(request: HttpRequest, id: str):
|
def detail_view(request: HttpRequest, id: str):
|
||||||
""" Renders a detail view for a compensation
|
""" Renders a detail view for a compensation
|
||||||
|
|
||||||
@ -257,7 +259,7 @@ def detail_view(request: HttpRequest, id: str):
|
|||||||
"last_checked_tooltip": last_checked_tooltip,
|
"last_checked_tooltip": last_checked_tooltip,
|
||||||
"geom_form": geom_form,
|
"geom_form": geom_form,
|
||||||
"parcels": parcels,
|
"parcels": parcels,
|
||||||
"has_access": is_data_shared,
|
"is_entry_shared": is_data_shared,
|
||||||
"actions": actions,
|
"actions": actions,
|
||||||
"before_states": before_states,
|
"before_states": before_states,
|
||||||
"after_states": after_states,
|
"after_states": after_states,
|
||||||
|
@ -67,7 +67,7 @@ def report_view(request: HttpRequest, id: str):
|
|||||||
"img": qrcode_img_lanis,
|
"img": qrcode_img_lanis,
|
||||||
"url": qrcode_lanis_url,
|
"url": qrcode_lanis_url,
|
||||||
},
|
},
|
||||||
"has_access": False, # disables action buttons during rendering
|
"is_entry_shared": False, # disables action buttons during rendering
|
||||||
"before_states": before_states,
|
"before_states": before_states,
|
||||||
"after_states": after_states,
|
"after_states": after_states,
|
||||||
"geom_form": geom_form,
|
"geom_form": geom_form,
|
||||||
|
@ -17,7 +17,8 @@ from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm
|
|||||||
from compensation.models import EcoAccount
|
from compensation.models import EcoAccount
|
||||||
from compensation.tables.eco_account import EcoAccountTable
|
from compensation.tables.eco_account import EcoAccountTable
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal
|
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
|
||||||
|
uuid_required
|
||||||
from konova.forms import SimpleGeomForm
|
from konova.forms import SimpleGeomForm
|
||||||
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
|
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
|
||||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||||
@ -177,6 +178,7 @@ def edit_view(request: HttpRequest, id: str):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@any_group_check
|
@any_group_check
|
||||||
|
@uuid_required
|
||||||
def detail_view(request: HttpRequest, id: str):
|
def detail_view(request: HttpRequest, id: str):
|
||||||
""" Renders a detail view for a compensation
|
""" Renders a detail view for a compensation
|
||||||
|
|
||||||
@ -235,7 +237,7 @@ def detail_view(request: HttpRequest, id: str):
|
|||||||
"obj": acc,
|
"obj": acc,
|
||||||
"geom_form": geom_form,
|
"geom_form": geom_form,
|
||||||
"parcels": parcels,
|
"parcels": parcels,
|
||||||
"has_access": is_data_shared,
|
"is_entry_shared": is_data_shared,
|
||||||
"before_states": before_states,
|
"before_states": before_states,
|
||||||
"after_states": after_states,
|
"after_states": after_states,
|
||||||
"sum_before_states": sum_before_states,
|
"sum_before_states": sum_before_states,
|
||||||
|
@ -73,7 +73,7 @@ def report_view(request: HttpRequest, id: str):
|
|||||||
"img": qrcode_img_lanis,
|
"img": qrcode_img_lanis,
|
||||||
"url": qrcode_lanis_url,
|
"url": qrcode_lanis_url,
|
||||||
},
|
},
|
||||||
"has_access": False, # disables action buttons during rendering
|
"is_entry_shared": False, # disables action buttons during rendering
|
||||||
"before_states": before_states,
|
"before_states": before_states,
|
||||||
"after_states": after_states,
|
"after_states": after_states,
|
||||||
"geom_form": geom_form,
|
"geom_form": geom_form,
|
||||||
|
@ -16,4 +16,5 @@ class EmaAdmin(AbstractCompensationAdmin):
|
|||||||
"teams",
|
"teams",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Ema, EmaAdmin)
|
admin.site.register(Ema, EmaAdmin)
|
||||||
|
@ -133,7 +133,6 @@ class EditEmaForm(NewEmaForm):
|
|||||||
def save(self, user: User, geom_form: SimpleGeomForm):
|
def save(self, user: User, geom_form: SimpleGeomForm):
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
# Fetch data from cleaned POST values
|
# Fetch data from cleaned POST values
|
||||||
identifier = self.cleaned_data.get("identifier", None)
|
|
||||||
title = self.cleaned_data.get("title", None)
|
title = self.cleaned_data.get("title", None)
|
||||||
handler_type = self.cleaned_data.get("handler_type", None)
|
handler_type = self.cleaned_data.get("handler_type", None)
|
||||||
handler_detail = self.cleaned_data.get("handler_detail", None)
|
handler_detail = self.cleaned_data.get("handler_detail", None)
|
||||||
@ -154,7 +153,6 @@ class EditEmaForm(NewEmaForm):
|
|||||||
self.instance.responsible.save()
|
self.instance.responsible.save()
|
||||||
|
|
||||||
# Update main oject data
|
# Update main oject data
|
||||||
self.instance.identifier = identifier
|
|
||||||
self.instance.title = title
|
self.instance.title = title
|
||||||
self.instance.comment = comment
|
self.instance.comment = comment
|
||||||
self.instance.is_pik = is_pik
|
self.instance.is_pik = is_pik
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-action' obj.id %}" title="{% trans 'Add new action' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-action' obj.id %}" title="{% trans 'Add new action' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'seedling' %}
|
{% fa5_icon 'seedling' %}
|
||||||
@ -49,7 +49,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for detail in action.action_type_details.all %}
|
{% for detail in action.action_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'ema:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
|
<button data-form-url="{% url 'ema:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
{% fa5_icon 'file-alt' %}
|
{% fa5_icon 'file-alt' %}
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}">
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}">
|
||||||
{% fa5_icon 'bell' %}
|
{% fa5_icon 'bell' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'calendar-check' %}
|
{% fa5_icon 'calendar-check' %}
|
||||||
@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'ema:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
|
<button data-form-url="{% url 'ema:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'file' %}
|
{% fa5_icon 'file' %}
|
||||||
@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'ema:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
<button data-form-url="{% url 'ema:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'layer-group' %}
|
{% fa5_icon 'layer-group' %}
|
||||||
@ -49,14 +49,14 @@
|
|||||||
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
<br>
|
<br>
|
||||||
{% for detail in state.biotope_type_details.all %}
|
{% for detail in state.biotope_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'layer-group' %}
|
{% fa5_icon 'layer-group' %}
|
||||||
@ -49,14 +49,14 @@
|
|||||||
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
<br>
|
<br>
|
||||||
{% for detail in state.biotope_type_details.all %}
|
{% for detail in state.biotope_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -87,7 +87,7 @@
|
|||||||
{% include 'user/includes/team_data_modal_button.html' %}
|
{% include 'user/includes/team_data_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<hr>
|
<hr>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
{% for user in obj.users.all %}
|
{% for user in obj.users.all %}
|
||||||
{% include 'user/includes/contact_modal_button.html' %}
|
{% include 'user/includes/contact_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -80,6 +80,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
self.ema = self.fill_out_ema(self.ema)
|
self.ema = self.fill_out_ema(self.ema)
|
||||||
pre_edit_log_count = self.ema.log.count()
|
pre_edit_log_count = self.ema.log.count()
|
||||||
|
|
||||||
|
old_identifier = self.ema.identifier
|
||||||
new_title = self.create_dummy_string()
|
new_title = self.create_dummy_string()
|
||||||
new_identifier = self.create_dummy_string()
|
new_identifier = self.create_dummy_string()
|
||||||
new_comment = self.create_dummy_string()
|
new_comment = self.create_dummy_string()
|
||||||
@ -106,13 +107,13 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
|
|
||||||
check_on_elements = {
|
check_on_elements = {
|
||||||
self.ema.title: new_title,
|
self.ema.title: new_title,
|
||||||
self.ema.identifier: new_identifier,
|
|
||||||
self.ema.comment: new_comment,
|
self.ema.comment: new_comment,
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v in check_on_elements.items():
|
for k, v in check_on_elements.items():
|
||||||
self.assertEqual(k, v)
|
self.assertEqual(k, v)
|
||||||
|
|
||||||
|
self.assertEqual(self.ema.identifier, old_identifier)
|
||||||
self.assert_equal_geometries(self.ema.geometry.geom, new_geometry)
|
self.assert_equal_geometries(self.ema.geometry.geom, new_geometry)
|
||||||
|
|
||||||
# Expect logs to be set
|
# Expect logs to be set
|
||||||
|
@ -130,7 +130,7 @@ class EditEmaFormTestCase(BaseTestCase):
|
|||||||
self.assertIsNotNone(obj.responsible.handler)
|
self.assertIsNotNone(obj.responsible.handler)
|
||||||
self.assertEqual(obj.responsible.conservation_office, data["conservation_office"])
|
self.assertEqual(obj.responsible.conservation_office, data["conservation_office"])
|
||||||
self.assertEqual(obj.responsible.conservation_file_number, data["conservation_file_number"])
|
self.assertEqual(obj.responsible.conservation_file_number, data["conservation_file_number"])
|
||||||
self.assertEqual(obj.identifier, data["identifier"])
|
self.assertNotEqual(obj.identifier, data["identifier"], msg="Identifier editable via form!")
|
||||||
self.assertEqual(obj.comment, data["comment"])
|
self.assertEqual(obj.comment, data["comment"])
|
||||||
|
|
||||||
last_log = obj.log.first()
|
last_log = obj.log.first()
|
||||||
|
@ -17,7 +17,8 @@ from ema.forms import NewEmaForm, EditEmaForm
|
|||||||
from ema.models import Ema
|
from ema.models import Ema
|
||||||
from ema.tables import EmaTable
|
from ema.tables import EmaTable
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
|
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal, \
|
||||||
|
uuid_required
|
||||||
from konova.forms import SimpleGeomForm
|
from konova.forms import SimpleGeomForm
|
||||||
from konova.forms.modals import RemoveModalForm
|
from konova.forms.modals import RemoveModalForm
|
||||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
||||||
@ -124,6 +125,7 @@ def new_id_view(request: HttpRequest):
|
|||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
|
@uuid_required
|
||||||
def detail_view(request: HttpRequest, id: str):
|
def detail_view(request: HttpRequest, id: str):
|
||||||
""" Renders the detail view of an EMA
|
""" Renders the detail view of an EMA
|
||||||
|
|
||||||
@ -140,7 +142,7 @@ def detail_view(request: HttpRequest, id: str):
|
|||||||
geom_form = SimpleGeomForm(instance=ema)
|
geom_form = SimpleGeomForm(instance=ema)
|
||||||
parcels = ema.get_underlying_parcels()
|
parcels = ema.get_underlying_parcels()
|
||||||
_user = request.user
|
_user = request.user
|
||||||
is_data_shared = ema.is_shared_with(_user)
|
is_entry_shared = ema.is_shared_with(_user)
|
||||||
|
|
||||||
# Order states according to surface
|
# Order states according to surface
|
||||||
before_states = ema.before_states.all().order_by("-surface")
|
before_states = ema.before_states.all().order_by("-surface")
|
||||||
@ -165,7 +167,7 @@ def detail_view(request: HttpRequest, id: str):
|
|||||||
"obj": ema,
|
"obj": ema,
|
||||||
"geom_form": geom_form,
|
"geom_form": geom_form,
|
||||||
"parcels": parcels,
|
"parcels": parcels,
|
||||||
"has_access": is_data_shared,
|
"is_entry_shared": is_entry_shared,
|
||||||
"before_states": before_states,
|
"before_states": before_states,
|
||||||
"after_states": after_states,
|
"after_states": after_states,
|
||||||
"sum_before_states": sum_before_states,
|
"sum_before_states": sum_before_states,
|
||||||
|
@ -67,7 +67,7 @@ def report_view(request:HttpRequest, id: str):
|
|||||||
"img": qrcode_img_lanis,
|
"img": qrcode_img_lanis,
|
||||||
"url": qrcode_lanis_url
|
"url": qrcode_lanis_url
|
||||||
},
|
},
|
||||||
"has_access": False, # disables action buttons during rendering
|
"is_entry_shared": False, # disables action buttons during rendering
|
||||||
"before_states": before_states,
|
"before_states": before_states,
|
||||||
"after_states": after_states,
|
"after_states": after_states,
|
||||||
"geom_form": geom_form,
|
"geom_form": geom_form,
|
||||||
|
@ -345,7 +345,6 @@ class EditInterventionForm(NewInterventionForm):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
with transaction.atomic():
|
with transaction.atomic():
|
||||||
identifier = self.cleaned_data.get("identifier", None)
|
|
||||||
title = self.cleaned_data.get("title", None)
|
title = self.cleaned_data.get("title", None)
|
||||||
process_type = self.cleaned_data.get("type", None)
|
process_type = self.cleaned_data.get("type", None)
|
||||||
laws = self.cleaned_data.get("laws", None)
|
laws = self.cleaned_data.get("laws", None)
|
||||||
@ -379,7 +378,6 @@ class EditInterventionForm(NewInterventionForm):
|
|||||||
|
|
||||||
self.instance.log.add(user_action)
|
self.instance.log.add(user_action)
|
||||||
|
|
||||||
self.instance.identifier = identifier
|
|
||||||
self.instance.title = title
|
self.instance.title = title
|
||||||
self.instance.comment = comment
|
self.instance.comment = comment
|
||||||
self.instance.modified = user_action
|
self.instance.modified = user_action
|
||||||
|
@ -33,7 +33,7 @@ class CheckModalForm(BaseModalForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.form_title = _("Run check")
|
self.form_title = _("Run check")
|
||||||
self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name)
|
self.form_caption = _("The necessary control steps have been performed:").format(self.user.first_name, self.user.last_name)
|
||||||
self.valid = False
|
self.valid = False
|
||||||
|
|
||||||
def _are_deductions_valid(self):
|
def _are_deductions_valid(self):
|
||||||
|
@ -5,6 +5,8 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
|||||||
Created on: 30.11.20
|
Created on: 30.11.20
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from konova.sub_settings.django_settings import env
|
||||||
|
|
||||||
INTERVENTION_IDENTIFIER_LENGTH = 6
|
INTERVENTION_IDENTIFIER_LENGTH = 6
|
||||||
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}"
|
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}"
|
||||||
|
|
||||||
@ -14,7 +16,7 @@ INTERVENTION_LANIS_LAYER_NAME_UNRECORDED_OLD_ENTRY = "eiv_unrecorded_old_entries
|
|||||||
|
|
||||||
# EGON connection settings via rabbitmq
|
# EGON connection settings via rabbitmq
|
||||||
# NEEDED FOR BACKWARDS COMPATIBILITY
|
# NEEDED FOR BACKWARDS COMPATIBILITY
|
||||||
EGON_RABBITMQ_HOST = "CHANGE_ME"
|
EGON_RABBITMQ_HOST = env("EGON_RABBITMQ_HOST")
|
||||||
EGON_RABBITMQ_PORT = "CHANGE_ME"
|
EGON_RABBITMQ_PORT = env("EGON_RABBITMQ_PORT")
|
||||||
EGON_RABBITMQ_USER = "CHANGE_ME"
|
EGON_RABBITMQ_USER = env("EGON_RABBITMQ_USER")
|
||||||
EGON_RABBITMQ_PW = "CHANGE_ME"
|
EGON_RABBITMQ_PW = env("EGON_RABBITMQ_PW")
|
||||||
|
@ -33,6 +33,11 @@ class InterventionTable(BaseTable, TableRenderMixin, TableOrderMixin):
|
|||||||
verbose_name=_("Parcel gmrkng"),
|
verbose_name=_("Parcel gmrkng"),
|
||||||
orderable=False,
|
orderable=False,
|
||||||
accessor="geometry",
|
accessor="geometry",
|
||||||
|
attrs={
|
||||||
|
"th": {
|
||||||
|
"class": "w-25",
|
||||||
|
}
|
||||||
|
}
|
||||||
)
|
)
|
||||||
c = tables.Column(
|
c = tables.Column(
|
||||||
verbose_name=_("Checked"),
|
verbose_name=_("Checked"),
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<a href="{% url 'compensation:new' obj.id %}" title="{% trans 'Add new compensation' %}">
|
<a href="{% url 'compensation:new' obj.id %}" title="{% trans 'Add new compensation' %}">
|
||||||
<button class="btn btn-outline-default">
|
<button class="btn btn-outline-default">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
@ -32,7 +32,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Title' %}
|
{% trans 'Title' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@ -51,7 +51,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="align-middle">{{ comp.title }}</td>
|
<td class="align-middle">{{ comp.title }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'intervention:remove-compensation' obj.id comp.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove compensation' %}">
|
<button data-form-url="{% url 'intervention:remove-compensation' obj.id comp.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove compensation' %}">
|
||||||
{% fa5_icon 'trash' %}
|
{% fa5_icon 'trash' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -11,7 +11,7 @@
|
|||||||
{% fa5_icon 'file-alt' %}
|
{% fa5_icon 'file-alt' %}
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'intervention:resubmission-create' obj.id %}">
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'intervention:resubmission-create' obj.id %}">
|
||||||
{% fa5_icon 'bell' %}
|
{% fa5_icon 'bell' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'tree' %}
|
{% fa5_icon 'tree' %}
|
||||||
@ -33,7 +33,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Created' %}
|
{% trans 'Created' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@ -56,7 +56,7 @@
|
|||||||
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
|
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
|
||||||
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
|
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'intervention:edit-deduction' obj.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
|
<button data-form-url="{% url 'intervention:edit-deduction' obj.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'file' %}
|
{% fa5_icon 'file' %}
|
||||||
@ -38,7 +38,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@ -66,7 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'intervention:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
<button data-form-url="{% url 'intervention:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:pay:new' obj.id %}" title="{% trans 'Add new payment' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:pay:new' obj.id %}" title="{% trans 'Add new payment' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'money-bill-wave' %}
|
{% fa5_icon 'money-bill-wave' %}
|
||||||
@ -33,7 +33,7 @@
|
|||||||
<th class="w-50" scope="col">
|
<th class="w-50" scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@ -46,16 +46,24 @@
|
|||||||
{% for pay in obj.payments.all %}
|
{% for pay in obj.payments.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
|
{% if is_entry_shared %}
|
||||||
{{ pay.amount|floatformat:2 }} €
|
{{ pay.amount|floatformat:2 }} €
|
||||||
|
{% else %}
|
||||||
|
***
|
||||||
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle">{{ pay.due_on|default_if_none:"---" }}</td>
|
<td class="align-middle">{{ pay.due_on|default_if_none:"---" }}</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
<div class="scroll-150">
|
<div class="scroll-150">
|
||||||
|
{% if is_entry_shared %}
|
||||||
{{ pay.comment }}
|
{{ pay.comment }}
|
||||||
|
{% else %}
|
||||||
|
{% trans 'This data is not shared with you' %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:pay:edit' obj.id pay.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit payment' %}">
|
<button data-form-url="{% url 'compensation:pay:edit' obj.id pay.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit payment' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -13,7 +13,7 @@
|
|||||||
{% comment %}
|
{% comment %}
|
||||||
Only show add-button if no revocation exists, yet.
|
Only show add-button if no revocation exists, yet.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% if is_default_member and has_access and not obj.legal.revocation %}
|
{% if is_default_member and is_entry_shared and not obj.legal.revocation %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-revocation' obj.id %}" title="{% trans 'Add revocation' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-revocation' obj.id %}" title="{% trans 'Add revocation' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'ban' %}
|
{% fa5_icon 'ban' %}
|
||||||
@ -36,7 +36,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@ -64,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'intervention:edit-revocation' obj.id rev.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit revocation' %}">
|
<button data-form-url="{% url 'intervention:edit-revocation' obj.id rev.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit revocation' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
@ -129,7 +129,7 @@
|
|||||||
{% include 'user/includes/team_data_modal_button.html' %}
|
{% include 'user/includes/team_data_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<hr>
|
<hr>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
{% for user in obj.users.all %}
|
{% for user in obj.users.all %}
|
||||||
{% include 'user/includes/contact_modal_button.html' %}
|
{% include 'user/includes/contact_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
@ -146,17 +146,17 @@ class EgonGmlBuilder:
|
|||||||
|
|
||||||
def _gen_foto(self):
|
def _gen_foto(self):
|
||||||
revoc_docs, regular_docs = self.intervention.get_documents()
|
revoc_docs, regular_docs = self.intervention.get_documents()
|
||||||
docs_list = [
|
docs_list = {
|
||||||
|
"oneo:Foto": [
|
||||||
{
|
{
|
||||||
"oneo:Foto": {
|
|
||||||
"oneo:aufnahmezeitpunkt": doc.date_of_creation.strftime(DEFAULT_DATE_FORMAT),
|
"oneo:aufnahmezeitpunkt": doc.date_of_creation.strftime(DEFAULT_DATE_FORMAT),
|
||||||
"oneo:bemerkung": doc.comment,
|
"oneo:bemerkung": doc.comment,
|
||||||
"oneo:fotoverweis": base64.b64encode(doc.file.read()).decode("utf-8"),
|
"oneo:fotoverweis": base64.b64encode(doc.file.read()).decode("utf-8"),
|
||||||
"oneo:dateiname": doc.file.url.split("/")[-1],
|
"oneo:dateiname": doc.file.url.split("/")[-1],
|
||||||
"oneo:hauptfoto": False,
|
"oneo:hauptfoto": False,
|
||||||
}
|
|
||||||
} for doc in regular_docs
|
} for doc in regular_docs
|
||||||
]
|
]
|
||||||
|
}
|
||||||
return docs_list
|
return docs_list
|
||||||
|
|
||||||
def build_gml(self):
|
def build_gml(self):
|
||||||
|
@ -16,7 +16,8 @@ from intervention.forms.intervention import EditInterventionForm, NewInterventio
|
|||||||
from intervention.models import Intervention
|
from intervention.models import Intervention
|
||||||
from intervention.tables import InterventionTable
|
from intervention.tables import InterventionTable
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
from konova.decorators import default_group_required, shared_access_required, any_group_check, login_required_modal
|
from konova.decorators import default_group_required, shared_access_required, any_group_check, login_required_modal, \
|
||||||
|
uuid_required
|
||||||
from konova.forms import SimpleGeomForm
|
from konova.forms import SimpleGeomForm
|
||||||
from konova.forms.modals import RemoveModalForm
|
from konova.forms.modals import RemoveModalForm
|
||||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
||||||
@ -39,7 +40,7 @@ def index_view(request: HttpRequest):
|
|||||||
"""
|
"""
|
||||||
template = "generic_index.html"
|
template = "generic_index.html"
|
||||||
|
|
||||||
# Filtering by user access is performed in table filter inside of InterventionTableFilter class
|
# Filtering by user access is performed in table filter inside InterventionTableFilter class
|
||||||
interventions = Intervention.objects.filter(
|
interventions = Intervention.objects.filter(
|
||||||
deleted=None, # not deleted
|
deleted=None, # not deleted
|
||||||
).select_related(
|
).select_related(
|
||||||
@ -128,6 +129,7 @@ def new_id_view(request: HttpRequest):
|
|||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@any_group_check
|
@any_group_check
|
||||||
|
@uuid_required
|
||||||
def detail_view(request: HttpRequest, id: str):
|
def detail_view(request: HttpRequest, id: str):
|
||||||
""" Renders a detail view for viewing an intervention's data
|
""" Renders a detail view for viewing an intervention's data
|
||||||
|
|
||||||
@ -183,7 +185,7 @@ def detail_view(request: HttpRequest, id: str):
|
|||||||
"last_checked": last_checked,
|
"last_checked": last_checked,
|
||||||
"last_checked_tooltip": last_checked_tooltip,
|
"last_checked_tooltip": last_checked_tooltip,
|
||||||
"compensations": compensations,
|
"compensations": compensations,
|
||||||
"has_access": is_data_shared,
|
"is_entry_shared": is_data_shared,
|
||||||
"geom_form": geom_form,
|
"geom_form": geom_form,
|
||||||
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
||||||
"is_zb_member": _user.in_group(ZB_GROUP),
|
"is_zb_member": _user.in_group(ZB_GROUP),
|
||||||
|
@ -12,11 +12,13 @@ from django.utils.translation import gettext_lazy as _
|
|||||||
|
|
||||||
from intervention.models import Intervention
|
from intervention.models import Intervention
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
|
from konova.decorators import uuid_required
|
||||||
from konova.forms import SimpleGeomForm
|
from konova.forms import SimpleGeomForm
|
||||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||||
from konova.utils.generators import generate_qr_code
|
from konova.utils.generators import generate_qr_code
|
||||||
|
|
||||||
|
|
||||||
|
@uuid_required
|
||||||
def report_view(request: HttpRequest, id: str):
|
def report_view(request: HttpRequest, id: str):
|
||||||
""" Renders the public report view
|
""" Renders the public report view
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
|
from konova.sub_settings.django_settings import env
|
||||||
|
|
||||||
# Set the default Django settings module for the 'celery' program.
|
# Set the default Django settings module for the 'celery' program.
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'konova.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'konova.settings')
|
||||||
@ -17,7 +18,7 @@ app.config_from_object('django.conf:settings', namespace='CELERY')
|
|||||||
app.autodiscover_tasks()
|
app.autodiscover_tasks()
|
||||||
|
|
||||||
# Declare redis as broker
|
# Declare redis as broker
|
||||||
app.conf.broker_url = 'redis://localhost:6379/0'
|
app.conf.broker_url = f'redis://{env("REDIS_HOST")}:{env.int("REDIS_PORT")}/0'
|
||||||
|
|
||||||
|
|
||||||
@app.task(bind=True)
|
@app.task(bind=True)
|
||||||
|
@ -7,9 +7,11 @@ Created on: 16.11.20
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
from uuid import UUID
|
||||||
|
|
||||||
from bootstrap_modal_forms.mixins import is_ajax
|
from bootstrap_modal_forms.mixins import is_ajax
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
|
from django.http import Http404
|
||||||
from django.shortcuts import redirect, get_object_or_404, render
|
from django.shortcuts import redirect, get_object_or_404, render
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
@ -171,3 +173,20 @@ def login_required_modal(function):
|
|||||||
return render(request, template, context)
|
return render(request, template, context)
|
||||||
return function(request, *args, **kwargs)
|
return function(request, *args, **kwargs)
|
||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
|
def uuid_required(function):
|
||||||
|
"""
|
||||||
|
Checks whether the given input is a valid UUID
|
||||||
|
"""
|
||||||
|
@wraps(function)
|
||||||
|
def wrap(request, *args, **kwargs):
|
||||||
|
uuid = kwargs.get("uuid", None) or kwargs.get("id", None)
|
||||||
|
try:
|
||||||
|
uuid = UUID(uuid)
|
||||||
|
except ValueError:
|
||||||
|
raise Http404(
|
||||||
|
"Invalid UUID"
|
||||||
|
)
|
||||||
|
return function(request, *args, **kwargs)
|
||||||
|
return wrap
|
||||||
|
55
konova/filters/mixins/user_log.py
Normal file
55
konova/filters/mixins/user_log.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||||
|
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||||
|
Created on: 19.08.24
|
||||||
|
|
||||||
|
"""
|
||||||
|
import django_filters
|
||||||
|
from django import forms
|
||||||
|
from django.db.models import QuerySet, Q
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class UserLoggedTableFilterMixin(django_filters.FilterSet):
|
||||||
|
ul = django_filters.CharFilter(
|
||||||
|
method="filter_user_log",
|
||||||
|
label=_(""),
|
||||||
|
label_suffix=_(""),
|
||||||
|
widget=forms.TextInput(
|
||||||
|
attrs={
|
||||||
|
"placeholder": _("Logged user"),
|
||||||
|
"title": _("Search for entries where this person has been participated according to log history"),
|
||||||
|
"class": "form-control",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def filter_user_log(self, queryset, name, value) -> QuerySet:
|
||||||
|
""" Filters queryset depending on value of input
|
||||||
|
|
||||||
|
Args:
|
||||||
|
queryset (QuerySet): Incoming (prefiltered) queryset
|
||||||
|
name (str): Name of input field
|
||||||
|
value (str): Value of input field
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
value = value.replace(",", " ")
|
||||||
|
value = value.strip()
|
||||||
|
values = value.split(" ")
|
||||||
|
|
||||||
|
q = Q()
|
||||||
|
for val in values:
|
||||||
|
q &= (
|
||||||
|
Q(log__user__username__icontains=val) |
|
||||||
|
Q(log__user__first_name__icontains=val) |
|
||||||
|
Q(log__user__last_name__icontains=val)
|
||||||
|
)
|
||||||
|
|
||||||
|
queryset = queryset.filter(q)
|
||||||
|
return queryset
|
@ -14,6 +14,7 @@ from konova.filters.mixins.office import ConservationOfficeTableFilterMixin, Reg
|
|||||||
from konova.filters.mixins.record import RecordableTableFilterMixin
|
from konova.filters.mixins.record import RecordableTableFilterMixin
|
||||||
from konova.filters.mixins.self_created import SelfCreatedTableFilterMixin
|
from konova.filters.mixins.self_created import SelfCreatedTableFilterMixin
|
||||||
from konova.filters.mixins.share import ShareableTableFilterMixin
|
from konova.filters.mixins.share import ShareableTableFilterMixin
|
||||||
|
from konova.filters.mixins.user_log import UserLoggedTableFilterMixin
|
||||||
|
|
||||||
|
|
||||||
class AbstractTableFilter(django_filters.FilterSet):
|
class AbstractTableFilter(django_filters.FilterSet):
|
||||||
@ -40,7 +41,8 @@ class SelectionTableFilter(RegistrationOfficeTableFilterMixin,
|
|||||||
|
|
||||||
class QueryTableFilter(KeywordTableFilterMixin,
|
class QueryTableFilter(KeywordTableFilterMixin,
|
||||||
FileNumberTableFilterMixin,
|
FileNumberTableFilterMixin,
|
||||||
GeoReferencedTableFilterMixin):
|
GeoReferencedTableFilterMixin,
|
||||||
|
UserLoggedTableFilterMixin):
|
||||||
""" TableFilter holding different filter options for query related filtering
|
""" TableFilter holding different filter options for query related filtering
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
@ -98,12 +98,14 @@ class SimpleGeomForm(BaseForm):
|
|||||||
|
|
||||||
if g.geom_type not in accepted_ogr_types:
|
if g.geom_type not in accepted_ogr_types:
|
||||||
self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
|
self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
|
||||||
is_valid = False
|
is_valid &= False
|
||||||
return is_valid
|
return is_valid
|
||||||
|
|
||||||
|
is_valid &= self.__is_area_valid(g)
|
||||||
|
|
||||||
polygon = Polygon.from_ewkt(g.ewkt)
|
polygon = Polygon.from_ewkt(g.ewkt)
|
||||||
is_valid = polygon.valid
|
is_valid &= polygon.valid
|
||||||
if not is_valid:
|
if not polygon.valid:
|
||||||
self.add_error("geom", polygon.valid_reason)
|
self.add_error("geom", polygon.valid_reason)
|
||||||
return is_valid
|
return is_valid
|
||||||
|
|
||||||
@ -137,6 +139,24 @@ class SimpleGeomForm(BaseForm):
|
|||||||
|
|
||||||
return num_vertices <= GEOM_MAX_VERTICES
|
return num_vertices <= GEOM_MAX_VERTICES
|
||||||
|
|
||||||
|
def __is_area_valid(self, geom: gdal.OGRGeometry):
|
||||||
|
""" Checks whether the area is at least > 1m²
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
is_area_valid = geom.area > 1 # > 1m² (SRID:25832)
|
||||||
|
|
||||||
|
if not is_area_valid:
|
||||||
|
self.add_error(
|
||||||
|
"geom",
|
||||||
|
_("Geometry must be greater than 1m². Currently is {}m²").format(
|
||||||
|
float(geom.area)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
return is_area_valid
|
||||||
|
|
||||||
def __simplify_geometry(self, geom, max_vert: int):
|
def __simplify_geometry(self, geom, max_vert: int):
|
||||||
""" Simplifies a geometry
|
""" Simplifies a geometry
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ class RecordModalForm(BaseModalForm):
|
|||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.form_title = _("Record data")
|
self.form_title = _("Record data")
|
||||||
self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name)
|
self.form_caption = _("The necessary control steps have been performed:").format(self.user.first_name, self.user.last_name)
|
||||||
# Disable automatic w-100 setting for this type of modal form. Looks kinda strange
|
# Disable automatic w-100 setting for this type of modal form. Looks kinda strange
|
||||||
self.fields["confirm"].widget.attrs["class"] = ""
|
self.fields["confirm"].widget.attrs["class"] = ""
|
||||||
|
|
||||||
|
@ -6,11 +6,11 @@ Created on: 26.10.22
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from datetime import datetime
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.datetime_safe import datetime
|
|
||||||
|
|
||||||
from analysis.utils.excel.excel import TempExcelFile
|
from analysis.utils.excel.excel import TempExcelFile
|
||||||
from analysis.utils.report import TimespanReport
|
from analysis.utils.report import TimespanReport
|
||||||
|
88
konova/management/commands/recalculate_parcels.py
Normal file
88
konova/management/commands/recalculate_parcels.py
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||||
|
Contact: michel.peltriaux@sgdnord.rlp.de
|
||||||
|
Created on: 04.01.22
|
||||||
|
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.contrib.gis.db.models.functions import Area
|
||||||
|
|
||||||
|
from konova.management.commands.setup import BaseKonovaCommand
|
||||||
|
from konova.models import Geometry, ParcelIntersection
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseKonovaCommand):
|
||||||
|
help = "Recalculates parcels for entries with geometry but missing parcel information"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--force-all",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="If Attribute set, all entries parcels will be recalculated"
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
try:
|
||||||
|
self.recalculate_parcels(options)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self._break_line()
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
def recalculate_parcels(self, options: dict):
|
||||||
|
force_all = options.get("force_all", False)
|
||||||
|
|
||||||
|
geometry_objects = Geometry.objects.filter(
|
||||||
|
geom__isempty=False,
|
||||||
|
).exclude(
|
||||||
|
geom=None
|
||||||
|
)
|
||||||
|
|
||||||
|
if not force_all:
|
||||||
|
# Fetch all intersections
|
||||||
|
intersection_objs = ParcelIntersection.objects.filter(
|
||||||
|
geometry__in=geometry_objects
|
||||||
|
)
|
||||||
|
# Just take the geometry ids, which seem to have intersections
|
||||||
|
geom_with_intersection_ids = intersection_objs.values_list(
|
||||||
|
"geometry__id",
|
||||||
|
flat=True
|
||||||
|
)
|
||||||
|
# ... and resolve into Geometry objects again ...
|
||||||
|
intersected_geom_objs = Geometry.objects.filter(
|
||||||
|
id__in=geom_with_intersection_ids
|
||||||
|
)
|
||||||
|
# ... to be able to use the way more efficient difference() function ...
|
||||||
|
geometry_objects_ids = geometry_objects.difference(intersected_geom_objs).values_list("id", flat=True)
|
||||||
|
# ... so we can resolve these into proper Geometry objects again for further annotation usage
|
||||||
|
geometry_objects = Geometry.objects.filter(id__in=geometry_objects_ids)
|
||||||
|
|
||||||
|
self._write_warning("=== Update parcels and districts ===")
|
||||||
|
# Order geometries by size to process smaller once at first
|
||||||
|
geometries = geometry_objects.annotate(
|
||||||
|
area=Area("geom")
|
||||||
|
).order_by(
|
||||||
|
'area'
|
||||||
|
)
|
||||||
|
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
|
||||||
|
i = 0
|
||||||
|
num_geoms = geometries.count()
|
||||||
|
geoms_with_errors = {}
|
||||||
|
for geometry in geometries:
|
||||||
|
self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
|
||||||
|
try:
|
||||||
|
geometry.update_parcels()
|
||||||
|
self._write_warning(f"--- Processed {geometry.get_underlying_parcels().count()} underlying parcels")
|
||||||
|
except Exception as e:
|
||||||
|
geoms_with_errors[geometry.id] = str(e)
|
||||||
|
i += 1
|
||||||
|
self._write_warning(f"--- {i}/{num_geoms} processed")
|
||||||
|
|
||||||
|
self._write_success("Updating parcels done!")
|
||||||
|
|
||||||
|
for key, val in geoms_with_errors.items():
|
||||||
|
self._write_error(f" Error on {key}: {val}")
|
||||||
|
self._write_success(f"{num_geoms - len(geoms_with_errors)} geometries successfuly recalculated!")
|
||||||
|
self._break_line()
|
@ -61,14 +61,24 @@ class Command(BaseKonovaCommand):
|
|||||||
action=UserAction.CREATED
|
action=UserAction.CREATED
|
||||||
)
|
)
|
||||||
|
|
||||||
intervention_log_entries_ids = self.get_all_log_entries_ids(Intervention)
|
EIV_log_entries_ids = self.get_all_log_entries_ids(Intervention)
|
||||||
attached_log_entries_id = intervention_log_entries_ids.union(
|
self._write_warning(f" EIV: {EIV_log_entries_ids.count()} attached log entries")
|
||||||
self.get_all_log_entries_ids(Compensation),
|
KOM_log_entries_ids = self.get_all_log_entries_ids(Compensation)
|
||||||
self.get_all_log_entries_ids(EcoAccount),
|
self._write_warning(f" KOM: {KOM_log_entries_ids.count()} attached log entries")
|
||||||
self.get_all_log_entries_ids(Ema),
|
OEK_log_entries_ids = self.get_all_log_entries_ids(EcoAccount)
|
||||||
)
|
self._write_warning(f" OEK: {OEK_log_entries_ids.count()} attached log entries")
|
||||||
|
EMA_log_entries_ids = self.get_all_log_entries_ids(Ema)
|
||||||
|
self._write_warning(f" EMA: {EMA_log_entries_ids.count()} attached log entries")
|
||||||
|
|
||||||
unattached_log_entries = all_log_entries.exclude(id__in=attached_log_entries_id)
|
unattached_log_entries = all_log_entries.exclude(
|
||||||
|
id__in=EIV_log_entries_ids
|
||||||
|
).exclude(
|
||||||
|
id__in=KOM_log_entries_ids
|
||||||
|
).exclude(
|
||||||
|
id__in=OEK_log_entries_ids
|
||||||
|
).exclude(
|
||||||
|
id__in=EMA_log_entries_ids
|
||||||
|
)
|
||||||
|
|
||||||
num_entries = unattached_log_entries.count()
|
num_entries = unattached_log_entries.count()
|
||||||
if num_entries > 0:
|
if num_entries > 0:
|
||||||
@ -108,13 +118,20 @@ class Command(BaseKonovaCommand):
|
|||||||
self._write_warning("=== Sanitize compensation actions ===")
|
self._write_warning("=== Sanitize compensation actions ===")
|
||||||
all_actions = CompensationAction.objects.all()
|
all_actions = CompensationAction.objects.all()
|
||||||
|
|
||||||
compensation_action_ids = self.get_all_action_ids(Compensation)
|
kom_action_ids = self.get_all_action_ids(Compensation)
|
||||||
attached_action_ids = compensation_action_ids.union(
|
self._write_warning(f" KOM: {kom_action_ids.count()} attached actions")
|
||||||
self.get_all_action_ids(EcoAccount),
|
oek_action_ids = self.get_all_action_ids(EcoAccount)
|
||||||
self.get_all_action_ids(Ema),
|
self._write_warning(f" OEK: {oek_action_ids.count()} attached actions")
|
||||||
)
|
ema_action_ids = self.get_all_action_ids(Ema)
|
||||||
|
self._write_warning(f" EMA: {ema_action_ids.count()} attached actions")
|
||||||
|
|
||||||
unattached_actions = all_actions.exclude(id__in=attached_action_ids)
|
unattached_actions = all_actions.exclude(
|
||||||
|
id__in=kom_action_ids
|
||||||
|
).exclude(
|
||||||
|
id__in=oek_action_ids
|
||||||
|
).exclude(
|
||||||
|
id__in=ema_action_ids
|
||||||
|
)
|
||||||
|
|
||||||
num_entries = unattached_actions.count()
|
num_entries = unattached_actions.count()
|
||||||
if num_entries > 0:
|
if num_entries > 0:
|
||||||
@ -125,7 +142,7 @@ class Command(BaseKonovaCommand):
|
|||||||
self._write_success("No unattached actions found.")
|
self._write_success("No unattached actions found.")
|
||||||
self._break_line()
|
self._break_line()
|
||||||
|
|
||||||
def get_all_deadline_ids(self, cls):
|
def _get_all_deadline_ids(self, cls):
|
||||||
""" Getter for all deadline ids of a model
|
""" Getter for all deadline ids of a model
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -154,13 +171,20 @@ class Command(BaseKonovaCommand):
|
|||||||
self._write_warning("=== Sanitize deadlines ===")
|
self._write_warning("=== Sanitize deadlines ===")
|
||||||
all_deadlines = Deadline.objects.all()
|
all_deadlines = Deadline.objects.all()
|
||||||
|
|
||||||
compensation_deadline_ids = self.get_all_deadline_ids(Compensation)
|
kom_deadline_ids = self._get_all_deadline_ids(Compensation)
|
||||||
attached_deadline_ids = compensation_deadline_ids.union(
|
self._write_warning(f" KOM: {kom_deadline_ids.count()} attached deadlines")
|
||||||
self.get_all_deadline_ids(EcoAccount),
|
oek_deadline_ids = self._get_all_deadline_ids(EcoAccount)
|
||||||
self.get_all_deadline_ids(Ema),
|
self._write_warning(f" OEK: {kom_deadline_ids.count()} attached deadlines")
|
||||||
)
|
ema_deadline_ids = self._get_all_deadline_ids(Ema)
|
||||||
|
self._write_warning(f" EMA: {kom_deadline_ids.count()} attached deadlines")
|
||||||
|
|
||||||
unattached_deadlines = all_deadlines.exclude(id__in=attached_deadline_ids)
|
unattached_deadlines = all_deadlines.exclude(
|
||||||
|
id__in=kom_deadline_ids
|
||||||
|
).exclude(
|
||||||
|
id__in=oek_deadline_ids
|
||||||
|
).exclude(
|
||||||
|
id__in=ema_deadline_ids
|
||||||
|
)
|
||||||
|
|
||||||
num_entries = unattached_deadlines.count()
|
num_entries = unattached_deadlines.count()
|
||||||
if num_entries > 0:
|
if num_entries > 0:
|
||||||
@ -171,7 +195,7 @@ class Command(BaseKonovaCommand):
|
|||||||
self._write_success("No unattached deadlines found.")
|
self._write_success("No unattached deadlines found.")
|
||||||
self._break_line()
|
self._break_line()
|
||||||
|
|
||||||
def get_all_geometry_ids(self, cls):
|
def _get_all_geometry_ids(self, cls):
|
||||||
""" Getter for all geometry ids of a model
|
""" Getter for all geometry ids of a model
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -200,14 +224,24 @@ class Command(BaseKonovaCommand):
|
|||||||
self._write_warning("=== Sanitize geometries ===")
|
self._write_warning("=== Sanitize geometries ===")
|
||||||
all_geometries = Geometry.objects.all()
|
all_geometries = Geometry.objects.all()
|
||||||
|
|
||||||
compensation_geometry_ids = self.get_all_geometry_ids(Compensation)
|
kom_geometry_ids = self._get_all_geometry_ids(Compensation)
|
||||||
attached_geometry_ids = compensation_geometry_ids.union(
|
self._write_warning(f" KOM: {kom_geometry_ids.count()} attached geometries")
|
||||||
self.get_all_geometry_ids(Intervention),
|
eiv_geometry_ids = self._get_all_geometry_ids(Intervention)
|
||||||
self.get_all_geometry_ids(EcoAccount),
|
self._write_warning(f" EIV: {eiv_geometry_ids.count()} attached geometries")
|
||||||
self.get_all_geometry_ids(Ema),
|
oek_geometry_ids = self._get_all_geometry_ids(EcoAccount)
|
||||||
)
|
self._write_warning(f" OEK: {oek_geometry_ids.count()} attached geometries")
|
||||||
|
ema_geometry_ids = self._get_all_geometry_ids(Ema)
|
||||||
|
self._write_warning(f" EMA: {ema_geometry_ids.count()} attached geometries")
|
||||||
|
|
||||||
unattached_geometries = all_geometries.exclude(id__in=attached_geometry_ids)
|
unattached_geometries = all_geometries.exclude(
|
||||||
|
id__in=kom_geometry_ids
|
||||||
|
).exclude(
|
||||||
|
id__in=eiv_geometry_ids
|
||||||
|
).exclude(
|
||||||
|
id__in=oek_geometry_ids
|
||||||
|
).exclude(
|
||||||
|
id__in=ema_geometry_ids
|
||||||
|
)
|
||||||
|
|
||||||
num_entries = unattached_geometries.count()
|
num_entries = unattached_geometries.count()
|
||||||
if num_entries > 0:
|
if num_entries > 0:
|
||||||
@ -218,7 +252,7 @@ class Command(BaseKonovaCommand):
|
|||||||
self._write_success("No unattached geometries found.")
|
self._write_success("No unattached geometries found.")
|
||||||
self._break_line()
|
self._break_line()
|
||||||
|
|
||||||
def get_all_state_ids(self, cls):
|
def _get_all_state_ids(self, cls):
|
||||||
""" Getter for all states (before and after) of a class
|
""" Getter for all states (before and after) of a class
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
@ -254,14 +288,19 @@ class Command(BaseKonovaCommand):
|
|||||||
"""
|
"""
|
||||||
self._write_warning("=== Sanitize compensation states ===")
|
self._write_warning("=== Sanitize compensation states ===")
|
||||||
all_states = CompensationState.objects.all()
|
all_states = CompensationState.objects.all()
|
||||||
compensation_state_ids = self.get_all_state_ids(Compensation)
|
|
||||||
account_state_ids = self.get_all_state_ids(EcoAccount)
|
kom_state_ids = self._get_all_state_ids(Compensation)
|
||||||
ema_state_ids = self.get_all_state_ids(Ema)
|
oek_state_ids = self._get_all_state_ids(EcoAccount)
|
||||||
attached_state_ids = compensation_state_ids.union(account_state_ids, ema_state_ids)
|
ema_state_ids = self._get_all_state_ids(Ema)
|
||||||
|
|
||||||
unattached_states = all_states.exclude(
|
unattached_states = all_states.exclude(
|
||||||
id__in=attached_state_ids
|
id__in=kom_state_ids
|
||||||
|
).exclude(
|
||||||
|
id__in=oek_state_ids
|
||||||
|
).exclude(
|
||||||
|
id__in=ema_state_ids
|
||||||
)
|
)
|
||||||
|
|
||||||
num_unattached_states = unattached_states.count()
|
num_unattached_states = unattached_states.count()
|
||||||
if num_unattached_states > 0:
|
if num_unattached_states > 0:
|
||||||
self._write_error(f"Found {num_unattached_states} unused compensation states. Delete now...")
|
self._write_error(f"Found {num_unattached_states} unused compensation states. Delete now...")
|
||||||
|
54
konova/management/commands/send_to_egon.py
Normal file
54
konova/management/commands/send_to_egon.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||||
|
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||||
|
Created on: 18.06.24
|
||||||
|
|
||||||
|
"""
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
|
from intervention.models import Intervention
|
||||||
|
from konova.management.commands.setup import BaseKonovaCommand
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseKonovaCommand):
|
||||||
|
help = "Send specific intervention entries to EGON if there are any payments on them"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
try:
|
||||||
|
parser.add_argument("--intervention-ids", type=str)
|
||||||
|
except ValueError as e:
|
||||||
|
self._write_error(f"Argument error: {e}")
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
def __handle_arguments(self, options):
|
||||||
|
self.intervention_ids = options["intervention_ids"] or ""
|
||||||
|
self.intervention_ids = self.intervention_ids.split(",")
|
||||||
|
self.intervention_ids = [x.strip() for x in self.intervention_ids]
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
try:
|
||||||
|
self.__handle_arguments(options)
|
||||||
|
interventions = self.get_interventions()
|
||||||
|
self.process_egon_sending(interventions)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self._break_line()
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
def get_interventions(self) -> QuerySet:
|
||||||
|
"""
|
||||||
|
Getter for interventions, defined by parameter 'intervention-ids'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
interventions (QuerySet): The interventions
|
||||||
|
"""
|
||||||
|
interventions = Intervention.objects.filter(
|
||||||
|
id__in=self.intervention_ids,
|
||||||
|
)
|
||||||
|
self._write_success(f"... Found {interventions.count()} interventions")
|
||||||
|
return interventions
|
||||||
|
|
||||||
|
def process_egon_sending(self, interventions: QuerySet):
|
||||||
|
for intervention in interventions:
|
||||||
|
intervention.send_data_to_egon()
|
||||||
|
self._write_warning(f"... {intervention.identifier} has been sent to EGON (if it has payments)")
|
@ -1,51 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Michel Peltriaux
|
|
||||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
|
||||||
Contact: michel.peltriaux@sgdnord.rlp.de
|
|
||||||
Created on: 19.08.21
|
|
||||||
|
|
||||||
"""
|
|
||||||
from django.core.management import BaseCommand
|
|
||||||
|
|
||||||
from intervention.models import Intervention
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "Performs test on collisions using the identifier generation"
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
identifiers = {}
|
|
||||||
max_iterations = 100000
|
|
||||||
try:
|
|
||||||
collisions = 0
|
|
||||||
len_ids = len(identifiers)
|
|
||||||
while len_ids < max_iterations:
|
|
||||||
tmp_intervention = Intervention()
|
|
||||||
_id = tmp_intervention.generate_new_identifier()
|
|
||||||
len_ids = len(identifiers)
|
|
||||||
if _id not in identifiers:
|
|
||||||
if len_ids % (max_iterations/5) == 0:
|
|
||||||
print(len_ids)
|
|
||||||
identifiers[_id] = None
|
|
||||||
else:
|
|
||||||
collisions += 1
|
|
||||||
print("+++ Collision after {} identifiers +++".format(len_ids))
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
self._break_line()
|
|
||||||
exit(-1)
|
|
||||||
print(
|
|
||||||
"\n{} collisions in {} identifiers; Collision rate {}%".format(
|
|
||||||
collisions,
|
|
||||||
len_ids,
|
|
||||||
(collisions / len_ids)*100,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _break_line(self):
|
|
||||||
""" Simply prints a line break
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.stdout.write("\n")
|
|
@ -1,54 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Michel Peltriaux
|
|
||||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
|
||||||
Contact: michel.peltriaux@sgdnord.rlp.de
|
|
||||||
Created on: 04.01.22
|
|
||||||
|
|
||||||
"""
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from django.contrib.gis.db.models.functions import Area
|
|
||||||
|
|
||||||
from konova.management.commands.setup import BaseKonovaCommand
|
|
||||||
from konova.models import Geometry, Parcel, District
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseKonovaCommand):
|
|
||||||
help = "Checks the database' sanity and removes unused entries"
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
try:
|
|
||||||
self.update_all_parcels()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
self._break_line()
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
def update_all_parcels(self):
|
|
||||||
num_parcels_before = Parcel.objects.count()
|
|
||||||
num_districts_before = District.objects.count()
|
|
||||||
self._write_warning("=== Update parcels and districts ===")
|
|
||||||
# Order geometries by size to process smaller once at first
|
|
||||||
geometries = Geometry.objects.all().exclude(
|
|
||||||
geom=None
|
|
||||||
).annotate(area=Area("geom")).order_by(
|
|
||||||
'area'
|
|
||||||
)
|
|
||||||
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
|
|
||||||
i = 0
|
|
||||||
num_geoms = geometries.count()
|
|
||||||
for geometry in geometries:
|
|
||||||
self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
|
|
||||||
geometry.update_parcels()
|
|
||||||
self._write_warning(f"--- Processed {geometry.get_underlying_parcels().count()} underlying parcels")
|
|
||||||
i += 1
|
|
||||||
self._write_warning(f"--- {i}/{num_geoms} processed")
|
|
||||||
|
|
||||||
num_parcels_after = Parcel.objects.count()
|
|
||||||
num_districts_after = District.objects.count()
|
|
||||||
if num_parcels_after != num_parcels_before:
|
|
||||||
self._write_error(f"Parcels have changed: {num_parcels_before} to {num_parcels_after} entries. You should run the sanitize command.")
|
|
||||||
if num_districts_after != num_districts_before:
|
|
||||||
self._write_error(f"Districts have changed: {num_districts_before} to {num_districts_after} entries. You should run the sanitize command.")
|
|
||||||
|
|
||||||
self._write_success("Updating parcels done!")
|
|
||||||
self._break_line()
|
|
@ -0,0 +1,23 @@
|
|||||||
|
# Generated by Django 5.0.1 on 2024-01-09 10:38
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('konova', '0014_resubmission'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='geometry',
|
||||||
|
name='parcel_update_end',
|
||||||
|
field=models.DateTimeField(blank=True, db_comment='When the last parcel calculation finished', help_text='When the last parcel calculation finished', null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='geometry',
|
||||||
|
name='parcel_update_start',
|
||||||
|
field=models.DateTimeField(blank=True, db_comment='When the last parcel calculation started', help_text='When the last parcel calculation started', null=True),
|
||||||
|
),
|
||||||
|
]
|
@ -0,0 +1,17 @@
|
|||||||
|
# Generated by Django 5.0.1 on 2024-02-16 07:34
|
||||||
|
|
||||||
|
from django.db import migrations
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('konova', '0015_geometry_parcel_calculation_end_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.RemoveField(
|
||||||
|
model_name='parcelintersection',
|
||||||
|
name='calculated_on',
|
||||||
|
),
|
||||||
|
]
|
@ -8,19 +8,31 @@ Created on: 15.11.21
|
|||||||
import json
|
import json
|
||||||
|
|
||||||
from django.contrib.gis.db.models import MultiPolygonField
|
from django.contrib.gis.db.models import MultiPolygonField
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
|
||||||
from konova.models import BaseResource, UuidModel
|
from konova.models import BaseResource, UuidModel
|
||||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
|
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
|
||||||
from konova.utils.schneider.fetcher import ParcelFetcher
|
from konova.utils.schneider.fetcher import ParcelFetcher
|
||||||
from konova.utils.wfs.spatial import ParcelWFSFetcher
|
|
||||||
|
|
||||||
|
|
||||||
class Geometry(BaseResource):
|
class Geometry(BaseResource):
|
||||||
"""
|
"""
|
||||||
Geometry model
|
Geometry model
|
||||||
"""
|
"""
|
||||||
|
parcel_update_start = models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
db_comment="When the last parcel calculation started",
|
||||||
|
help_text="When the last parcel calculation started"
|
||||||
|
)
|
||||||
|
parcel_update_end = models.DateTimeField(
|
||||||
|
blank=True,
|
||||||
|
null=True,
|
||||||
|
db_comment="When the last parcel calculation finished",
|
||||||
|
help_text="When the last parcel calculation finished",
|
||||||
|
)
|
||||||
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID_RLP)
|
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID_RLP)
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
@ -109,82 +121,14 @@ class Geometry(BaseResource):
|
|||||||
objs += set_objs
|
objs += set_objs
|
||||||
return objs
|
return objs
|
||||||
|
|
||||||
@transaction.atomic
|
def get_data_object(self):
|
||||||
def update_parcels_wfs(self):
|
|
||||||
""" Updates underlying parcel information using the WFS of LVermGeo
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup
|
Getter for the specific data object which is related to this geometry
|
||||||
|
"""
|
||||||
if self.geom.empty:
|
objs = self.get_data_objects()
|
||||||
# Nothing to do
|
assert (len(objs) <= 1)
|
||||||
return
|
result = objs.pop()
|
||||||
|
return result
|
||||||
parcel_fetcher = ParcelWFSFetcher(
|
|
||||||
geometry_id=self.id,
|
|
||||||
)
|
|
||||||
typename = "ave:Flurstueck"
|
|
||||||
fetched_parcels = parcel_fetcher.get_features(
|
|
||||||
typename
|
|
||||||
)
|
|
||||||
_now = timezone.now()
|
|
||||||
underlying_parcels = []
|
|
||||||
for result in fetched_parcels:
|
|
||||||
parcel_properties = result["properties"]
|
|
||||||
# There could be parcels which include the word 'Flur',
|
|
||||||
# which needs to be deleted and just keep the numerical values
|
|
||||||
## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE!
|
|
||||||
flr_val = parcel_properties["flur"].replace("Flur ", "")
|
|
||||||
district = District.objects.get_or_create(
|
|
||||||
key=parcel_properties["kreisschl"],
|
|
||||||
name=parcel_properties["kreis"],
|
|
||||||
)[0]
|
|
||||||
municipal = Municipal.objects.get_or_create(
|
|
||||||
key=parcel_properties["gmdschl"],
|
|
||||||
name=parcel_properties["gemeinde"],
|
|
||||||
district=district,
|
|
||||||
)[0]
|
|
||||||
parcel_group = ParcelGroup.objects.get_or_create(
|
|
||||||
key=parcel_properties["gemaschl"],
|
|
||||||
name=parcel_properties["gemarkung"],
|
|
||||||
municipal=municipal,
|
|
||||||
)[0]
|
|
||||||
flrstck_nnr = parcel_properties['flstnrnen']
|
|
||||||
if not flrstck_nnr:
|
|
||||||
flrstck_nnr = None
|
|
||||||
flrstck_zhlr = parcel_properties['flstnrzae']
|
|
||||||
if not flrstck_zhlr:
|
|
||||||
flrstck_zhlr = None
|
|
||||||
parcel_obj = Parcel.objects.get_or_create(
|
|
||||||
district=district,
|
|
||||||
municipal=municipal,
|
|
||||||
parcel_group=parcel_group,
|
|
||||||
flr=flr_val,
|
|
||||||
flrstck_nnr=flrstck_nnr,
|
|
||||||
flrstck_zhlr=flrstck_zhlr,
|
|
||||||
)[0]
|
|
||||||
parcel_obj.district = district
|
|
||||||
parcel_obj.updated_on = _now
|
|
||||||
parcel_obj.save()
|
|
||||||
underlying_parcels.append(parcel_obj)
|
|
||||||
|
|
||||||
# Update the linked parcels
|
|
||||||
self.parcels.clear()
|
|
||||||
self.parcels.set(underlying_parcels)
|
|
||||||
|
|
||||||
# Set the calculated_on intermediate field, so this related data will be found on lookups
|
|
||||||
intersections_without_ts = self.parcelintersection_set.filter(
|
|
||||||
parcel__in=self.parcels.all(),
|
|
||||||
calculated_on__isnull=True,
|
|
||||||
)
|
|
||||||
for entry in intersections_without_ts:
|
|
||||||
entry.calculated_on = _now
|
|
||||||
ParcelIntersection.objects.bulk_update(
|
|
||||||
intersections_without_ts,
|
|
||||||
["calculated_on"]
|
|
||||||
)
|
|
||||||
|
|
||||||
def update_parcels(self):
|
def update_parcels(self):
|
||||||
""" Updates underlying parcel information
|
""" Updates underlying parcel information
|
||||||
@ -192,72 +136,152 @@ class Geometry(BaseResource):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup
|
|
||||||
|
|
||||||
if self.geom.empty:
|
if self.geom.empty:
|
||||||
# Nothing to do
|
# Nothing to do
|
||||||
return
|
return
|
||||||
|
|
||||||
|
self._set_parcel_update_start_time()
|
||||||
|
self._perform_parcel_update()
|
||||||
|
self._set_parcel_update_end_time()
|
||||||
|
|
||||||
|
def _perform_parcel_update(self):
|
||||||
|
"""
|
||||||
|
Performs the main logic of parcel updating.
|
||||||
|
"""
|
||||||
|
from konova.models import Parcel, District, Municipal, ParcelGroup
|
||||||
|
|
||||||
parcel_fetcher = ParcelFetcher(
|
parcel_fetcher = ParcelFetcher(
|
||||||
geometry=self
|
geometry=self
|
||||||
)
|
)
|
||||||
fetched_parcels = parcel_fetcher.get_parcels()
|
fetched_parcels = parcel_fetcher.get_parcels()
|
||||||
|
|
||||||
_now = timezone.now()
|
_now = timezone.now()
|
||||||
underlying_parcels = []
|
|
||||||
|
districts = {}
|
||||||
|
municipals = {}
|
||||||
|
parcel_groups = {}
|
||||||
|
|
||||||
|
parcels_to_update = []
|
||||||
|
parcels_to_create = []
|
||||||
for result in fetched_parcels:
|
for result in fetched_parcels:
|
||||||
# There could be parcels which include the word 'Flur',
|
# There could be parcels which include the word 'Flur',
|
||||||
# which needs to be deleted and just keep the numerical values
|
# which needs to be deleted and just keep the numerical values
|
||||||
## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE!
|
## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE!
|
||||||
flr_val = result["flur"].replace("Flur ", "")
|
flr_val = result["flur"].replace("Flur ", "")
|
||||||
|
|
||||||
|
# Get district (cache in dict)
|
||||||
|
try:
|
||||||
|
district = districts["kreisschl"]
|
||||||
|
except KeyError:
|
||||||
district = District.objects.get_or_create(
|
district = District.objects.get_or_create(
|
||||||
key=result["kreisschl"],
|
key=result["kreisschl"],
|
||||||
name=result["kreis"],
|
name=result["kreis"],
|
||||||
)[0]
|
)[0]
|
||||||
|
districts[district.key] = district
|
||||||
|
|
||||||
|
# Get municipal (cache in dict)
|
||||||
|
try:
|
||||||
|
municipal = municipals["gmdschl"]
|
||||||
|
except KeyError:
|
||||||
municipal = Municipal.objects.get_or_create(
|
municipal = Municipal.objects.get_or_create(
|
||||||
key=result["gmdschl"],
|
key=result["gmdschl"],
|
||||||
name=result["gemeinde"],
|
name=result["gemeinde"],
|
||||||
district=district,
|
district=district,
|
||||||
)[0]
|
)[0]
|
||||||
|
municipals[municipal.key] = municipal
|
||||||
|
|
||||||
|
# Get parcel group (cache in dict)
|
||||||
|
try:
|
||||||
|
parcel_group = parcel_groups["gemaschl"]
|
||||||
|
except KeyError:
|
||||||
parcel_group = ParcelGroup.objects.get_or_create(
|
parcel_group = ParcelGroup.objects.get_or_create(
|
||||||
key=result["gemaschl"],
|
key=result["gemaschl"],
|
||||||
name=result["gemarkung"],
|
name=result["gemarkung"],
|
||||||
municipal=municipal,
|
municipal=municipal,
|
||||||
)[0]
|
)[0]
|
||||||
|
parcel_groups[parcel_group.key] = parcel_group
|
||||||
|
|
||||||
|
# Preprocess parcel data
|
||||||
flrstck_nnr = result['flstnrnen']
|
flrstck_nnr = result['flstnrnen']
|
||||||
if not flrstck_nnr:
|
match flrstck_nnr:
|
||||||
|
case "":
|
||||||
flrstck_nnr = None
|
flrstck_nnr = None
|
||||||
|
|
||||||
flrstck_zhlr = result['flstnrzae']
|
flrstck_zhlr = result['flstnrzae']
|
||||||
if not flrstck_zhlr:
|
match flrstck_zhlr:
|
||||||
|
case "":
|
||||||
flrstck_zhlr = None
|
flrstck_zhlr = None
|
||||||
parcel_obj = Parcel.objects.get_or_create(
|
|
||||||
|
try:
|
||||||
|
# Try to fetch parcel from db. If it already exists, just update timestamp.
|
||||||
|
parcel_obj = Parcel.objects.get(
|
||||||
district=district,
|
district=district,
|
||||||
municipal=municipal,
|
municipal=municipal,
|
||||||
parcel_group=parcel_group,
|
parcel_group=parcel_group,
|
||||||
flr=flr_val,
|
flr=flr_val,
|
||||||
flrstck_nnr=flrstck_nnr,
|
flrstck_nnr=flrstck_nnr,
|
||||||
flrstck_zhlr=flrstck_zhlr,
|
flrstck_zhlr=flrstck_zhlr,
|
||||||
)[0]
|
)
|
||||||
parcel_obj.district = district
|
|
||||||
parcel_obj.updated_on = _now
|
parcel_obj.updated_on = _now
|
||||||
parcel_obj.save()
|
parcels_to_update.append(parcel_obj)
|
||||||
underlying_parcels.append(parcel_obj)
|
except MultipleObjectsReturned:
|
||||||
|
parcel_obj = Parcel.make_unique(
|
||||||
|
district=district,
|
||||||
|
municipal=municipal,
|
||||||
|
parcel_group=parcel_group,
|
||||||
|
flr=flr_val,
|
||||||
|
flrstck_nnr=flrstck_nnr,
|
||||||
|
flrstck_zhlr=flrstck_zhlr,
|
||||||
|
)
|
||||||
|
parcel_obj.updated_on = _now
|
||||||
|
parcels_to_update.append(parcel_obj)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
# If not existing, create object but do not commit, yet
|
||||||
|
parcel_obj = Parcel(
|
||||||
|
district=district,
|
||||||
|
municipal=municipal,
|
||||||
|
parcel_group=parcel_group,
|
||||||
|
flr=flr_val,
|
||||||
|
flrstck_nnr=flrstck_nnr,
|
||||||
|
flrstck_zhlr=flrstck_zhlr,
|
||||||
|
updated_on=_now,
|
||||||
|
)
|
||||||
|
parcels_to_create.append(parcel_obj)
|
||||||
|
|
||||||
# Update the linked parcels
|
# Create new parcels
|
||||||
self.parcels.clear()
|
Parcel.objects.bulk_create(
|
||||||
|
parcels_to_create,
|
||||||
|
batch_size=500
|
||||||
|
)
|
||||||
|
# Update existing parcels
|
||||||
|
Parcel.objects.bulk_update(
|
||||||
|
parcels_to_update,
|
||||||
|
[
|
||||||
|
"updated_on"
|
||||||
|
],
|
||||||
|
batch_size=500
|
||||||
|
)
|
||||||
|
|
||||||
|
# Update linking to geometry
|
||||||
|
parcel_ids = [x.id for x in parcels_to_update] + [x.id for x in parcels_to_create]
|
||||||
|
underlying_parcels = Parcel.objects.filter(id__in=parcel_ids)
|
||||||
self.parcels.set(underlying_parcels)
|
self.parcels.set(underlying_parcels)
|
||||||
|
|
||||||
# Set the calculated_on intermediate field, so this related data will be found on lookups
|
@transaction.atomic
|
||||||
intersections_without_ts = self.parcelintersection_set.filter(
|
def _set_parcel_update_start_time(self):
|
||||||
parcel__in=self.parcels.all(),
|
"""
|
||||||
calculated_on__isnull=True,
|
Sets the current time for the parcel calculation begin
|
||||||
)
|
"""
|
||||||
for entry in intersections_without_ts:
|
self.parcel_update_start = timezone.now()
|
||||||
entry.calculated_on = _now
|
self.parcel_update_end = None
|
||||||
ParcelIntersection.objects.bulk_update(
|
self.save()
|
||||||
intersections_without_ts,
|
|
||||||
["calculated_on"]
|
@transaction.atomic
|
||||||
)
|
def _set_parcel_update_end_time(self):
|
||||||
|
"""
|
||||||
|
Sets the current time for the parcel calculation end
|
||||||
|
"""
|
||||||
|
self.parcel_update_end = timezone.now()
|
||||||
|
self.save()
|
||||||
|
|
||||||
def get_underlying_parcels(self):
|
def get_underlying_parcels(self):
|
||||||
""" Getter for related parcels and their districts
|
""" Getter for related parcels and their districts
|
||||||
@ -265,9 +289,7 @@ class Geometry(BaseResource):
|
|||||||
Returns:
|
Returns:
|
||||||
parcels (QuerySet): The related parcels as queryset
|
parcels (QuerySet): The related parcels as queryset
|
||||||
"""
|
"""
|
||||||
parcels = self.parcels.filter(
|
parcels = self.parcels.prefetch_related(
|
||||||
parcelintersection__calculated_on__isnull=False,
|
|
||||||
).prefetch_related(
|
|
||||||
"district",
|
"district",
|
||||||
"municipal",
|
"municipal",
|
||||||
).order_by(
|
).order_by(
|
||||||
@ -292,17 +314,6 @@ class Geometry(BaseResource):
|
|||||||
municipals = Municipal.objects.filter(id__in=municipals).order_by("name")
|
municipals = Municipal.objects.filter(id__in=municipals).order_by("name")
|
||||||
return municipals
|
return municipals
|
||||||
|
|
||||||
def count_underlying_parcels(self):
|
|
||||||
""" Getter for number of underlying parcels
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
num_parcels = self.parcels.filter(
|
|
||||||
parcelintersection__calculated_on__isnull=False,
|
|
||||||
).count()
|
|
||||||
return num_parcels
|
|
||||||
|
|
||||||
def as_feature_collection(self, srid=DEFAULT_SRID_RLP):
|
def as_feature_collection(self, srid=DEFAULT_SRID_RLP):
|
||||||
""" Returns a FeatureCollection structure holding all polygons of the MultiPolygon as single features
|
""" Returns a FeatureCollection structure holding all polygons of the MultiPolygon as single features
|
||||||
|
|
||||||
@ -337,6 +348,41 @@ class Geometry(BaseResource):
|
|||||||
}
|
}
|
||||||
return geojson
|
return geojson
|
||||||
|
|
||||||
|
@property
|
||||||
|
def complexity_factor(self) -> float:
|
||||||
|
""" Calculates a factor to estimate the complexity of a Geometry
|
||||||
|
|
||||||
|
0 = very low complexity
|
||||||
|
1 = very high complexity
|
||||||
|
|
||||||
|
ASSUMPTION:
|
||||||
|
The envelope is the bounding box of a geometry. If the geometry's area is similar to the area of it's bounding
|
||||||
|
box, it is considered as rather simple, since it seems to be a closer shape like a simple box.
|
||||||
|
If the geometry has a very big bounding box, but the geometry's own area is rather small,
|
||||||
|
compared to the one of the bounding box, the complexity can be higher.
|
||||||
|
|
||||||
|
Example:
|
||||||
|
geometry area similar to bounding box --> geometry / bounding_box ~ 1
|
||||||
|
geometry area far smaller than bb --> geometry / bounding_box ~ 0
|
||||||
|
|
||||||
|
Result is being inverted for better understanding of 'low' and 'high' complexity.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
complexity_factor (float): The estimated complexity
|
||||||
|
"""
|
||||||
|
if self.geom.empty:
|
||||||
|
return 0
|
||||||
|
|
||||||
|
geom_envelope = self.geom.envelope
|
||||||
|
diff = geom_envelope - self.geom
|
||||||
|
|
||||||
|
if diff.area == 0:
|
||||||
|
complexity_factor = 1
|
||||||
|
else:
|
||||||
|
complexity_factor = self.geom.area / diff.area
|
||||||
|
|
||||||
|
return complexity_factor
|
||||||
|
|
||||||
|
|
||||||
class GeometryConflict(UuidModel):
|
class GeometryConflict(UuidModel):
|
||||||
"""
|
"""
|
||||||
|
@ -672,17 +672,6 @@ class GeoReferencedMixin(models.Model):
|
|||||||
result = self.geometry.get_underlying_parcels()
|
result = self.geometry.get_underlying_parcels()
|
||||||
return result
|
return result
|
||||||
|
|
||||||
def count_underlying_parcels(self):
|
|
||||||
""" Getter for number of underlying parcels
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
result = 0
|
|
||||||
if self.geometry is not None:
|
|
||||||
result = self.geometry.count_underlying_parcels()
|
|
||||||
return result
|
|
||||||
|
|
||||||
def set_geometry_conflict_message(self, request: HttpRequest):
|
def set_geometry_conflict_message(self, request: HttpRequest):
|
||||||
if self.geometry is None:
|
if self.geometry is None:
|
||||||
return request
|
return request
|
||||||
|
@ -5,7 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
|||||||
Created on: 16.12.21
|
Created on: 16.12.21
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from django.db import models
|
from django.db import models, transaction
|
||||||
|
|
||||||
from konova.models import UuidModel
|
from konova.models import UuidModel
|
||||||
|
|
||||||
@ -158,21 +158,51 @@ class Parcel(UuidModel):
|
|||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"{self.parcel_group} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
|
return f"{self.parcel_group} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def make_unique(cls, **kwargs):
|
||||||
|
""" Checks for duplicates of a Parcel, choose a (now) unique one,
|
||||||
|
repairs relations for ParcelIntersection and removes duplicates.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
**kwargs ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
unique_true (Parcel): The new unique 'true one'
|
||||||
|
"""
|
||||||
|
parcel_objs = Parcel.objects.filter(**kwargs)
|
||||||
|
|
||||||
|
if not parcel_objs.exists():
|
||||||
|
return None
|
||||||
|
|
||||||
|
# Get one of the found parcels and use it as new 'true one'
|
||||||
|
unique_parcel = parcel_objs.first()
|
||||||
|
# separate it from the rest
|
||||||
|
parcel_objs = parcel_objs.exclude(id=unique_parcel.id)
|
||||||
|
|
||||||
|
if not parcel_objs.exists():
|
||||||
|
# There are no duplicates - all good, just return
|
||||||
|
return unique_parcel
|
||||||
|
|
||||||
|
# Fetch existing intersections, which still point on the duplicated parcels
|
||||||
|
intersection_objs = ParcelIntersection.objects.filter(
|
||||||
|
parcel__in=parcel_objs
|
||||||
|
)
|
||||||
|
|
||||||
|
# Change each intersection, so they point on the 'true one' parcel from now on
|
||||||
|
for intersection in intersection_objs:
|
||||||
|
intersection.parcel = unique_parcel
|
||||||
|
intersection.save()
|
||||||
|
|
||||||
|
# Remove the duplicated parcels
|
||||||
|
parcel_objs.delete()
|
||||||
|
|
||||||
|
return unique_parcel
|
||||||
|
|
||||||
|
|
||||||
class ParcelIntersection(UuidModel):
|
class ParcelIntersection(UuidModel):
|
||||||
""" ParcelIntersection is an intermediary model, which is used to configure the
|
"""
|
||||||
|
ParcelIntersection is an intermediary model, which is used to add extras to the
|
||||||
M2M relation between Parcel and Geometry.
|
M2M relation between Parcel and Geometry.
|
||||||
|
|
||||||
Based on uuids, we will not have (practically) any problems on outrunning primary keys
|
|
||||||
and extending the model with calculated_on timestamp, we can 'hide' entries while they
|
|
||||||
are being recalculated and keep track on the last time they have been calculated this
|
|
||||||
way.
|
|
||||||
|
|
||||||
Please note: The calculated_on describes when the relation between the Parcel and the Geometry
|
|
||||||
has been established. The updated_on field of Parcel describes when this Parcel has been
|
|
||||||
changed the last time.
|
|
||||||
|
|
||||||
"""
|
"""
|
||||||
parcel = models.ForeignKey(Parcel, on_delete=models.CASCADE)
|
parcel = models.ForeignKey(Parcel, on_delete=models.CASCADE)
|
||||||
geometry = models.ForeignKey("konova.Geometry", on_delete=models.CASCADE)
|
geometry = models.ForeignKey("konova.Geometry", on_delete=models.CASCADE)
|
||||||
calculated_on = models.DateTimeField(auto_now_add=True, null=True, blank=True)
|
|
||||||
|
@ -18,7 +18,6 @@ from konova.sub_settings.proxy_settings import *
|
|||||||
from konova.sub_settings.sso_settings import *
|
from konova.sub_settings.sso_settings import *
|
||||||
from konova.sub_settings.table_settings import *
|
from konova.sub_settings.table_settings import *
|
||||||
from konova.sub_settings.lanis_settings import *
|
from konova.sub_settings.lanis_settings import *
|
||||||
from konova.sub_settings.wfs_parcel_settings import *
|
|
||||||
from konova.sub_settings.logging_settings import *
|
from konova.sub_settings.logging_settings import *
|
||||||
|
|
||||||
# Max upload size for POST forms
|
# Max upload size for POST forms
|
||||||
@ -46,4 +45,8 @@ DEFAULT_GROUP = "Default"
|
|||||||
ZB_GROUP = "Registration office"
|
ZB_GROUP = "Registration office"
|
||||||
ETS_GROUP = "Conservation office"
|
ETS_GROUP = "Conservation office"
|
||||||
|
|
||||||
|
# GEOMETRY
|
||||||
|
## Max number of allowed vertices. Geometries larger will be simplified until they reach this threshold
|
||||||
GEOM_MAX_VERTICES = 10000
|
GEOM_MAX_VERTICES = 10000
|
||||||
|
## Max seconds to wait for a parcel calculation result before a new request will be started (default: 30 minutes)
|
||||||
|
GEOM_THRESHOLD_RECALCULATION_SECONDS = 60 * 30
|
||||||
|
@ -1,78 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Michel Peltriaux
|
|
||||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
|
||||||
Contact: michel.peltriaux@sgdnord.rlp.de
|
|
||||||
Created on: 17.08.21
|
|
||||||
|
|
||||||
"""
|
|
||||||
from django.http import HttpResponse
|
|
||||||
from django.urls import re_path
|
|
||||||
from django.views import View
|
|
||||||
from django.views.decorators.csrf import csrf_exempt
|
|
||||||
from itsdangerous import TimedSerializer
|
|
||||||
from simple_sso.sso_client.client import Client
|
|
||||||
|
|
||||||
from user.models import User
|
|
||||||
|
|
||||||
|
|
||||||
class PropagateView(View):
|
|
||||||
""" View used to receive propagated sso-server user data
|
|
||||||
|
|
||||||
"""
|
|
||||||
client = None
|
|
||||||
signer = None
|
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
|
||||||
super().__init__(*args, **kwargs)
|
|
||||||
self.signer = TimedSerializer(self.client.private_key)
|
|
||||||
|
|
||||||
@csrf_exempt
|
|
||||||
def dispatch(self, request, *args, **kwargs):
|
|
||||||
return super().dispatch(request, *args, **kwargs)
|
|
||||||
|
|
||||||
def post(self, request):
|
|
||||||
user_data = request.body
|
|
||||||
user_data = self.signer.loads(user_data)
|
|
||||||
self.client.build_user(user_data)
|
|
||||||
return HttpResponse(status=200)
|
|
||||||
|
|
||||||
|
|
||||||
class KonovaSSOClient(Client):
|
|
||||||
""" Konova specialized derivative of general sso.Client.
|
|
||||||
|
|
||||||
Adds some custom behaviour for konova usage.
|
|
||||||
|
|
||||||
"""
|
|
||||||
propagate_view = PropagateView
|
|
||||||
|
|
||||||
def get_urls(self):
|
|
||||||
urls = super().get_urls()
|
|
||||||
urls += re_path(r'^propagate/$', self.propagate_view.as_view(client=self), name='simple-sso-propagate'),
|
|
||||||
return urls
|
|
||||||
|
|
||||||
def build_user(self, user_data):
|
|
||||||
""" Creates a user or updates user data
|
|
||||||
|
|
||||||
Args:
|
|
||||||
user_data ():
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
user = User.objects.get(username=user_data['username'])
|
|
||||||
# Update user data, excluding some changes
|
|
||||||
skipable_attrs = {
|
|
||||||
"username",
|
|
||||||
"is_staff",
|
|
||||||
"is_superuser",
|
|
||||||
}
|
|
||||||
for _attr, _val in user_data.items():
|
|
||||||
if _attr in skipable_attrs:
|
|
||||||
continue
|
|
||||||
setattr(user, _attr, _val)
|
|
||||||
except User.DoesNotExist:
|
|
||||||
user = User(**user_data)
|
|
||||||
user.set_unusable_password()
|
|
||||||
user.save()
|
|
||||||
return user
|
|
@ -276,3 +276,6 @@ Similar to bootstraps 'shadow-lg'
|
|||||||
.tree-label.badge{
|
.tree-label.badge{
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
}
|
}
|
||||||
|
.alert{
|
||||||
|
margin-bottom: 0 !important;
|
||||||
|
}
|
@ -10,6 +10,8 @@ For the full list of settings and their values, see
|
|||||||
https://docs.djangoproject.com/en/3.1/ref/settings/
|
https://docs.djangoproject.com/en/3.1/ref/settings/
|
||||||
"""
|
"""
|
||||||
import os
|
import os
|
||||||
|
|
||||||
|
import environ
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from django.conf.locale.de import formats as de_formats
|
from django.conf.locale.de import formats as de_formats
|
||||||
|
|
||||||
@ -24,32 +26,28 @@ BASE_DIR = os.path.dirname(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
# Quick-start development settings - unsuitable for production
|
env = environ.Env()
|
||||||
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
# Take environment variables from .env.dev file
|
||||||
|
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
|
||||||
|
|
||||||
# SECURITY WARNING: keep the secret key used in production secret!
|
# SECURITY WARNING: keep the secret key used in production secret!
|
||||||
SECRET_KEY = '5=9-)2)h$u9=!zrhia9=lj-2#cpcb8=#$7y+)l$5tto$3q(n_+'
|
SECRET_KEY = env("SECRET_KEY")
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = True
|
DEBUG = env.bool("DEBUG", default=False)
|
||||||
|
|
||||||
ADMINS = [
|
ADMINS = [x.split(':') for x in env.list('ADMINS')]
|
||||||
('KSP-Servicestelle', 'ksp-servicestelle@sgdnord.rlp.de'),
|
|
||||||
]
|
|
||||||
|
|
||||||
BASE_URL = "http://localhost:8001"
|
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
|
||||||
|
|
||||||
ALLOWED_HOSTS = [
|
BASE_URL = env("BASE_URL")
|
||||||
"127.0.0.1",
|
|
||||||
"localhost",
|
|
||||||
]
|
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
"http://localhost", # not only host but schema (http/s) as well!
|
BASE_URL
|
||||||
]
|
]
|
||||||
|
|
||||||
# Authentication settings
|
# Authentication settings
|
||||||
LOGIN_URL = "/login/"
|
LOGIN_URL = "/oauth/login/"
|
||||||
|
|
||||||
# Session settings
|
# Session settings
|
||||||
SESSION_COOKIE_AGE = 60 * 60 # 60 minutes
|
SESSION_COOKIE_AGE = 60 * 60 # 60 minutes
|
||||||
@ -68,7 +66,6 @@ INSTALLED_APPS = [
|
|||||||
'django.contrib.staticfiles',
|
'django.contrib.staticfiles',
|
||||||
'django.contrib.gis',
|
'django.contrib.gis',
|
||||||
'django.contrib.humanize',
|
'django.contrib.humanize',
|
||||||
'simple_sso.sso_server',
|
|
||||||
'django_tables2',
|
'django_tables2',
|
||||||
'bootstrap_modal_forms',
|
'bootstrap_modal_forms',
|
||||||
'fontawesome_5',
|
'fontawesome_5',
|
||||||
@ -83,10 +80,6 @@ INSTALLED_APPS = [
|
|||||||
'analysis',
|
'analysis',
|
||||||
'api',
|
'api',
|
||||||
]
|
]
|
||||||
if DEBUG:
|
|
||||||
INSTALLED_APPS += [
|
|
||||||
'debug_toolbar',
|
|
||||||
]
|
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
@ -98,10 +91,6 @@ MIDDLEWARE = [
|
|||||||
'django.contrib.messages.middleware.MessageMiddleware',
|
'django.contrib.messages.middleware.MessageMiddleware',
|
||||||
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
'django.middleware.clickjacking.XFrameOptionsMiddleware',
|
||||||
]
|
]
|
||||||
if DEBUG:
|
|
||||||
MIDDLEWARE += [
|
|
||||||
"debug_toolbar.middleware.DebugToolbarMiddleware",
|
|
||||||
]
|
|
||||||
|
|
||||||
ROOT_URLCONF = 'konova.urls'
|
ROOT_URLCONF = 'konova.urls'
|
||||||
|
|
||||||
@ -131,13 +120,14 @@ WSGI_APPLICATION = 'konova.wsgi.application'
|
|||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.contrib.gis.db.backends.postgis',
|
'ENGINE': 'django.contrib.gis.db.backends.postgis',
|
||||||
'NAME': 'konova',
|
'NAME': env("DB_NAME"),
|
||||||
'USER': 'postgres',
|
'USER': env("DB_USER"),
|
||||||
'HOST': '127.0.0.1',
|
'PASSWORD': env("DB_PASSWORD"),
|
||||||
'PORT': '5432',
|
'HOST': env("DB_HOST"),
|
||||||
|
'PORT': env("DB_PORT"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
|
|
||||||
# Password validation
|
# Password validation
|
||||||
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
|
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
|
||||||
@ -201,28 +191,6 @@ STATICFILES_DIRS = [
|
|||||||
os.path.join(BASE_DIR, 'templates/map/client/libs'), # NETGIS map client files
|
os.path.join(BASE_DIR, 'templates/map/client/libs'), # NETGIS map client files
|
||||||
]
|
]
|
||||||
|
|
||||||
# DJANGO DEBUG TOOLBAR
|
|
||||||
INTERNAL_IPS = [
|
|
||||||
"127.0.0.1"
|
|
||||||
]
|
|
||||||
DEBUG_TOOLBAR_CONFIG = {
|
|
||||||
"DISABLE_PANELS": {
|
|
||||||
'debug_toolbar.panels.versions.VersionsPanel',
|
|
||||||
'debug_toolbar.panels.timer.TimerPanel',
|
|
||||||
'debug_toolbar.panels.settings.SettingsPanel',
|
|
||||||
'debug_toolbar.panels.headers.HeadersPanel',
|
|
||||||
'debug_toolbar.panels.request.RequestPanel',
|
|
||||||
'debug_toolbar.panels.sql.SQLPanel',
|
|
||||||
'debug_toolbar.panels.staticfiles.StaticFilesPanel',
|
|
||||||
'debug_toolbar.panels.templates.TemplatesPanel',
|
|
||||||
'debug_toolbar.panels.cache.CachePanel',
|
|
||||||
'debug_toolbar.panels.signals.SignalsPanel',
|
|
||||||
'debug_toolbar.panels.logging.LoggingPanel',
|
|
||||||
'debug_toolbar.panels.redirects.RedirectsPanel',
|
|
||||||
'debug_toolbar.panels.profiling.ProfilingPanel',
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
# EMAIL (see https://docs.djangoproject.com/en/dev/topics/email/)
|
# EMAIL (see https://docs.djangoproject.com/en/dev/topics/email/)
|
||||||
|
|
||||||
# CHANGE_ME !!! ONLY FOR DEVELOPMENT !!!
|
# CHANGE_ME !!! ONLY FOR DEVELOPMENT !!!
|
||||||
@ -230,13 +198,10 @@ if DEBUG:
|
|||||||
EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
|
EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
|
||||||
EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location
|
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 = env("DEFAULT_FROM_EMAIL") # The default email address for the 'from' element
|
||||||
SERVER_EMAIL = DEFAULT_FROM_EMAIL # The default email sender address, which is used by Django to send errors via mail
|
SERVER_EMAIL = DEFAULT_FROM_EMAIL # The default email sender address, which is used by Django to send errors via mail
|
||||||
EMAIL_HOST = "localhost"
|
EMAIL_HOST = env("SMTP_HOST")
|
||||||
EMAIL_REPLY_TO = "ksp-servicestelle@sgdnord.rlp.de"
|
EMAIL_REPLY_TO = env("REPLY_TO_ADDR")
|
||||||
SUPPORT_MAIL_RECIPIENT = EMAIL_REPLY_TO
|
EMAIL_PORT = env("SMTP_PORT")
|
||||||
EMAIL_PORT = "25"
|
|
||||||
#EMAIL_HOST_USER = ""
|
|
||||||
#EMAIL_HOST_PASSWORD = ""
|
|
||||||
EMAIL_USE_TLS = False
|
EMAIL_USE_TLS = False
|
||||||
EMAIL_USE_SSL = False
|
EMAIL_USE_SSL = False
|
||||||
|
@ -5,6 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
|||||||
Created on: 31.01.22
|
Created on: 31.01.22
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from konova.sub_settings.django_settings import env
|
||||||
|
|
||||||
# MAPS
|
# MAPS
|
||||||
DEFAULT_LAT = 50.00
|
DEFAULT_LAT = 50.00
|
||||||
@ -28,3 +29,6 @@ LANIS_ZOOM_LUT = {
|
|||||||
1000: 30,
|
1000: 30,
|
||||||
500: 31,
|
500: 31,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MAP_PROXY_HOST_WHITELIST = env.list("MAP_PROXY_HOST_WHITELIST")
|
||||||
|
i = 0
|
@ -5,12 +5,13 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
|||||||
Created on: 31.01.22
|
Created on: 31.01.22
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from konova.sub_settings.django_settings import env
|
||||||
|
|
||||||
proxy = ""
|
proxy = env("PROXY")
|
||||||
PROXIES = {
|
PROXIES = {
|
||||||
"http": proxy,
|
"http": proxy,
|
||||||
"https": proxy,
|
"https": proxy,
|
||||||
}
|
}
|
||||||
|
|
||||||
CLIENT_PROXY_AUTH_USER = "CHANGE_ME"
|
GEOPORTAL_RLP_USER = env("GEOPORTAL_RLP_USER")
|
||||||
CLIENT_PROXY_AUTH_PASSWORD = "CHANGE_ME"
|
GEOPORTAL_RLP_PASSWORD = env("GEOPORTAL_RLP_PASSWORD")
|
||||||
|
@ -5,7 +5,8 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
|||||||
Created on: 14.12.22
|
Created on: 14.12.22
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from konova.sub_settings.django_settings import env
|
||||||
|
|
||||||
base_url = "http://127.0.0.1:8002"
|
base_url = env("SCHNEIDER_BASE_URL")
|
||||||
auth_header = "auth"
|
auth_header = env("SCHNEIDER_AUTH_HEADER")
|
||||||
auth_header_token = "CHANGE_ME"
|
auth_header_token = env("SCHNEIDER_AUTH_TOKEN")
|
||||||
|
@ -5,9 +5,16 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
|||||||
Created on: 31.01.22
|
Created on: 31.01.22
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from konova.sub_settings.django_settings import env
|
||||||
|
|
||||||
# SSO settings
|
# SSO settings
|
||||||
SSO_SERVER_BASE = "http://127.0.0.1:8000/"
|
SSO_SERVER_BASE = env("SSO_SERVER_BASE_URL")
|
||||||
SSO_SERVER = f"{SSO_SERVER_BASE}sso/"
|
SSO_SERVER = f"{SSO_SERVER_BASE}sso/"
|
||||||
SSO_PRIVATE_KEY = "CHANGE_ME"
|
|
||||||
SSO_PUBLIC_KEY = "CHANGE_ME"
|
# OAuth settings
|
||||||
|
OAUTH_CODE_VERIFIER = env("OAUTH_CODE_VERIFIER")
|
||||||
|
|
||||||
|
OAUTH_CLIENT_ID = env("OAUTH_CLIENT_ID")
|
||||||
|
OAUTH_CLIENT_SECRET = env("OAUTH_CLIENT_SECRET")
|
||||||
|
|
||||||
|
PROPAGATION_SECRET = env("PROPAGATION_SECRET")
|
||||||
|
@ -1,12 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Michel Peltriaux
|
|
||||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
|
||||||
Contact: michel.peltriaux@sgdnord.rlp.de
|
|
||||||
Created on: 31.01.22
|
|
||||||
|
|
||||||
"""
|
|
||||||
|
|
||||||
# Parcel WFS settings
|
|
||||||
PARCEL_WFS_BASE_URL = "https://www.geoportal.rlp.de/registry/wfs/519"
|
|
||||||
PARCEL_WFS_USER = "ksp"
|
|
||||||
PARCEL_WFS_PW = "CHANGE_ME"
|
|
@ -7,18 +7,12 @@ from django.core.exceptions import ObjectDoesNotExist
|
|||||||
|
|
||||||
@shared_task
|
@shared_task
|
||||||
def celery_update_parcels(geometry_id: str, recheck: bool = True):
|
def celery_update_parcels(geometry_id: str, recheck: bool = True):
|
||||||
from konova.models import Geometry, ParcelIntersection
|
from konova.models import Geometry
|
||||||
try:
|
try:
|
||||||
geom = Geometry.objects.get(id=geometry_id)
|
geom = Geometry.objects.get(id=geometry_id)
|
||||||
objs = geom.parcelintersection_set.all()
|
geom.parcels.clear()
|
||||||
for obj in objs:
|
|
||||||
obj.calculated_on = None
|
|
||||||
ParcelIntersection.objects.bulk_update(
|
|
||||||
objs,
|
|
||||||
["calculated_on"]
|
|
||||||
)
|
|
||||||
|
|
||||||
geom.update_parcels()
|
geom.update_parcels()
|
||||||
|
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
if recheck:
|
if recheck:
|
||||||
sleep(5)
|
sleep(5)
|
||||||
|
@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="scroll-150 font-italic">
|
<div class="scroll-150 font-italic">
|
||||||
{{obj.comment}}
|
{{obj.comment|linebreaks}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user