Compare commits
No commits in common. "master" and "v1.3" have entirely different histories.
48
.env.sample
48
.env.sample
@ -1,48 +0,0 @@
|
|||||||
# 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,4 +3,3 @@
|
|||||||
/.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, OAuthToken
|
from api.models.token import APIUserToken
|
||||||
|
|
||||||
|
|
||||||
class APITokenAdmin(admin.ModelAdmin):
|
class APITokenAdmin(admin.ModelAdmin):
|
||||||
@ -17,17 +17,4 @@ 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)
|
|
||||||
|
@ -1,26 +0,0 @@
|
|||||||
# 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,14 +1,7 @@
|
|||||||
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
|
||||||
|
|
||||||
|
|
||||||
@ -51,129 +44,5 @@ 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("Token unknown")
|
raise PermissionError("Credentials invalid")
|
||||||
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,7 +11,6 @@ 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
|
||||||
@ -33,8 +32,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
|
||||||
@ -77,11 +76,7 @@ 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
|
||||||
@ -91,13 +86,7 @@ class AbstractModelAPISerializer:
|
|||||||
Returns:
|
Returns:
|
||||||
serialized_data (dict)
|
serialized_data (dict)
|
||||||
"""
|
"""
|
||||||
entries = self.model.objects.filter(
|
entries = self.model.objects.filter(**self.lookup).order_by("id")
|
||||||
**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,7 +6,6 @@ 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
|
||||||
@ -22,10 +21,8 @@ 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)
|
||||||
self.shared_lookup = Q(
|
del self.lookup["users__in"]
|
||||||
Q(intervention__users__in=[user]) |
|
self.lookup["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,7 +6,6 @@ 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
|
||||||
@ -29,11 +28,9 @@ 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.shared_lookup = Q(
|
self.lookup["intervention__users__in"] = [user]
|
||||||
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,8 +16,7 @@ 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_HANDLER_ID, \
|
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_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
|
||||||
@ -348,7 +347,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_FULL_ID) for e in entry["biotope_details"]
|
self._konova_code_from_json(e, CODELIST_BIOTOPES_EXTRA_CODES_ID) for e in entry["biotope_details"]
|
||||||
]
|
]
|
||||||
surface = float(entry["surface"])
|
surface = float(entry["surface"])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
@ -23,6 +23,11 @@ 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,19 +50,14 @@ 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
|
||||||
token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
|
ksp_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 not token and not ksp_user:
|
if ksp_user != token_user.username:
|
||||||
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}")
|
||||||
self.user = token_user
|
else:
|
||||||
|
self.user = token_user
|
||||||
|
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
if not self.user.is_default_user():
|
if not self.user.is_default_user():
|
||||||
|
@ -9,8 +9,7 @@ import collections
|
|||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
from codelist.settings import CODELIST_BIOTOPES_ID, \
|
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_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
|
||||||
|
|
||||||
@ -85,11 +84,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 = "short_name"
|
related_field_name = "long_name"
|
||||||
paginate_by = 200
|
paginate_by = 200
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.c = CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
self.c = CODELIST_BIOTOPES_EXTRA_CODES_ID
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def order_by(self, qs):
|
def order_by(self, qs):
|
||||||
@ -104,11 +103,8 @@ class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
|
|||||||
qs (QuerySet): The ordered queryset
|
qs (QuerySet): The ordered queryset
|
||||||
"""
|
"""
|
||||||
return qs.order_by(
|
return qs.order_by(
|
||||||
"short_name",
|
"long_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_BIOTOPES_EXTRA_CODES_FULL_ID
|
CODELIST_COMPENSATION_ACTION_DETAIL_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,7 +34,6 @@ 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,
|
||||||
@ -56,7 +55,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,
|
||||||
@ -75,15 +74,15 @@ class Command(BaseKonovaCommand):
|
|||||||
if items is None:
|
if items is None:
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
self._write_warning(" --- Found {} subcodes. Process now...".format(len(items)))
|
self._write_warning(" --- Found {} subcodes. Process now...".format(len(items)))
|
||||||
for element in items:
|
for element in items:
|
||||||
children = element.find("items")
|
children = element.find("items")
|
||||||
_id = element.find("id").text
|
_id = element.find("id").text
|
||||||
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 or ""
|
short_name = element.find("shortname").text
|
||||||
long_name = element.find("longname").text or ""
|
long_name = element.find("longname").text
|
||||||
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(
|
||||||
|
@ -1,60 +0,0 @@
|
|||||||
# 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))
|
|
||||||
|
|
@ -1,25 +0,0 @@
|
|||||||
# 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,11 +25,13 @@ 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="",
|
||||||
)
|
)
|
||||||
@ -48,28 +50,12 @@ 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
|
ret_val += self.parent.long_name + " > "
|
||||||
short_name = self.short_name
|
ret_val += self.long_name
|
||||||
|
if self.short_name and self.short_name != self.long_name:
|
||||||
both_names_exist = long_name is not None and short_name is not None
|
# 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})"
|
||||||
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 + " > "
|
|
||||||
elif parent_short_name_exists:
|
|
||||||
ret_val += self.parent.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
|
||||||
@ -89,8 +75,7 @@ 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,8 +15,7 @@ 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 - Subset of 288. Migration usage 975->288 in 08/2024
|
CODELIST_BIOTOPES_EXTRA_CODES_ID = 975 # CLZusatzbezeichnung
|
||||||
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,6 +213,7 @@ 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)
|
||||||
@ -220,6 +221,7 @@ 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,6 +192,7 @@ 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)
|
||||||
@ -218,6 +219,7 @@ 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,8 +14,7 @@ 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, \
|
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_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
|
||||||
@ -44,7 +43,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_FULL_ID],
|
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID],
|
||||||
),
|
),
|
||||||
widget=autocomplete.ModelSelect2Multiple(
|
widget=autocomplete.ModelSelect2Multiple(
|
||||||
url="codelist:biotope-extra-type-autocomplete",
|
url="codelist:biotope-extra-type-autocomplete",
|
||||||
|
@ -1,19 +0,0 @@
|
|||||||
# 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,6 +315,7 @@ 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, \
|
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_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_FULL_ID],
|
"code_lists__in": [CODELIST_BIOTOPES_EXTRA_CODES_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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if has_access %}
|
||||||
{% 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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared or is_default_member and user in deduction.intervention.shared_users %}
|
{% if is_default_member and has_access 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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if has_access %}
|
||||||
{% 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,16 +125,10 @@ 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(
|
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
|
||||||
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 = {
|
||||||
@ -157,21 +151,19 @@ 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)
|
||||||
|
|
||||||
# Expect identifier to not be editable
|
self.assert_equal_geometries(self.compensation.geometry.geom, new_geometry)
|
||||||
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,7 +82,6 @@ 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()
|
||||||
@ -115,6 +114,7 @@ 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,7 +123,6 @@ 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,8 +19,7 @@ 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
|
||||||
@ -201,7 +200,6 @@ 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
|
||||||
|
|
||||||
@ -259,7 +257,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,
|
||||||
"is_entry_shared": is_data_shared,
|
"has_access": 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,
|
||||||
},
|
},
|
||||||
"is_entry_shared": False, # disables action buttons during rendering
|
"has_access": 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,8 +17,7 @@ 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
|
||||||
@ -178,7 +177,6 @@ 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
|
||||||
|
|
||||||
@ -237,7 +235,7 @@ def detail_view(request: HttpRequest, id: str):
|
|||||||
"obj": acc,
|
"obj": acc,
|
||||||
"geom_form": geom_form,
|
"geom_form": geom_form,
|
||||||
"parcels": parcels,
|
"parcels": parcels,
|
||||||
"is_entry_shared": is_data_shared,
|
"has_access": 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,
|
||||||
},
|
},
|
||||||
"is_entry_shared": False, # disables action buttons during rendering
|
"has_access": 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,5 +16,4 @@ class EmaAdmin(AbstractCompensationAdmin):
|
|||||||
"teams",
|
"teams",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Ema, EmaAdmin)
|
admin.site.register(Ema, EmaAdmin)
|
||||||
|
@ -133,6 +133,7 @@ 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)
|
||||||
@ -153,6 +154,7 @@ 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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
<span class="badge badge-pill rlp-r" title="{{detail}}">{{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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if has_access %}
|
||||||
{% 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,7 +80,6 @@ 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()
|
||||||
@ -107,13 +106,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.assertNotEqual(obj.identifier, data["identifier"], msg="Identifier editable via form!")
|
self.assertEqual(obj.identifier, data["identifier"])
|
||||||
self.assertEqual(obj.comment, data["comment"])
|
self.assertEqual(obj.comment, data["comment"])
|
||||||
|
|
||||||
last_log = obj.log.first()
|
last_log = obj.log.first()
|
||||||
|
@ -17,8 +17,7 @@ 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
|
||||||
@ -125,7 +124,6 @@ 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
|
||||||
|
|
||||||
@ -142,7 +140,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_entry_shared = ema.is_shared_with(_user)
|
is_data_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")
|
||||||
@ -167,7 +165,7 @@ def detail_view(request: HttpRequest, id: str):
|
|||||||
"obj": ema,
|
"obj": ema,
|
||||||
"geom_form": geom_form,
|
"geom_form": geom_form,
|
||||||
"parcels": parcels,
|
"parcels": parcels,
|
||||||
"is_entry_shared": is_entry_shared,
|
"has_access": 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,
|
||||||
|
@ -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
|
||||||
},
|
},
|
||||||
"is_entry_shared": False, # disables action buttons during rendering
|
"has_access": 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,6 +345,7 @@ 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)
|
||||||
@ -378,6 +379,7 @@ 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 = _("The necessary control steps have been performed:").format(self.user.first_name, self.user.last_name)
|
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.valid = False
|
self.valid = False
|
||||||
|
|
||||||
def _are_deductions_valid(self):
|
def _are_deductions_valid(self):
|
||||||
|
@ -5,8 +5,6 @@ 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-{}"
|
||||||
|
|
||||||
@ -16,7 +14,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 = env("EGON_RABBITMQ_HOST")
|
EGON_RABBITMQ_HOST = "CHANGE_ME"
|
||||||
EGON_RABBITMQ_PORT = env("EGON_RABBITMQ_PORT")
|
EGON_RABBITMQ_PORT = "CHANGE_ME"
|
||||||
EGON_RABBITMQ_USER = env("EGON_RABBITMQ_USER")
|
EGON_RABBITMQ_USER = "CHANGE_ME"
|
||||||
EGON_RABBITMQ_PW = env("EGON_RABBITMQ_PW")
|
EGON_RABBITMQ_PW = "CHANGE_ME"
|
||||||
|
@ -33,11 +33,6 @@ 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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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,24 +46,16 @@
|
|||||||
{% 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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared and not obj.legal.revocation %}
|
{% if is_default_member and has_access 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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if is_default_member and has_access %}
|
||||||
<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 is_entry_shared %}
|
{% if has_access %}
|
||||||
{% 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 %}
|
||||||
|
@ -16,8 +16,7 @@ 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
|
||||||
@ -40,7 +39,7 @@ def index_view(request: HttpRequest):
|
|||||||
"""
|
"""
|
||||||
template = "generic_index.html"
|
template = "generic_index.html"
|
||||||
|
|
||||||
# Filtering by user access is performed in table filter inside InterventionTableFilter class
|
# Filtering by user access is performed in table filter inside of InterventionTableFilter class
|
||||||
interventions = Intervention.objects.filter(
|
interventions = Intervention.objects.filter(
|
||||||
deleted=None, # not deleted
|
deleted=None, # not deleted
|
||||||
).select_related(
|
).select_related(
|
||||||
@ -129,7 +128,6 @@ 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
|
||||||
|
|
||||||
@ -185,7 +183,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,
|
||||||
"is_entry_shared": is_data_shared,
|
"has_access": 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,13 +12,11 @@ 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,7 +1,6 @@
|
|||||||
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')
|
||||||
@ -18,7 +17,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 = f'redis://{env("REDIS_HOST")}:{env.int("REDIS_PORT")}/0'
|
app.conf.broker_url = 'redis://localhost:6379/0'
|
||||||
|
|
||||||
|
|
||||||
@app.task(bind=True)
|
@app.task(bind=True)
|
||||||
|
@ -7,11 +7,9 @@ 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 _
|
||||||
@ -173,20 +171,3 @@ 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
|
|
||||||
|
@ -1,55 +0,0 @@
|
|||||||
"""
|
|
||||||
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,7 +14,6 @@ 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):
|
||||||
@ -41,8 +40,7 @@ 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,14 +98,12 @@ 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 polygon.valid:
|
if not is_valid:
|
||||||
self.add_error("geom", polygon.valid_reason)
|
self.add_error("geom", polygon.valid_reason)
|
||||||
return is_valid
|
return is_valid
|
||||||
|
|
||||||
@ -139,24 +137,6 @@ 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 = _("The necessary control steps have been performed:").format(self.user.first_name, self.user.last_name)
|
self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").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
|
||||||
|
@ -1,88 +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, 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,25 +61,15 @@ class Command(BaseKonovaCommand):
|
|||||||
action=UserAction.CREATED
|
action=UserAction.CREATED
|
||||||
)
|
)
|
||||||
|
|
||||||
EIV_log_entries_ids = self.get_all_log_entries_ids(Intervention)
|
intervention_log_entries_ids = self.get_all_log_entries_ids(Intervention)
|
||||||
self._write_warning(f" EIV: {EIV_log_entries_ids.count()} attached log entries")
|
attached_log_entries_id = intervention_log_entries_ids.union(
|
||||||
KOM_log_entries_ids = self.get_all_log_entries_ids(Compensation)
|
self.get_all_log_entries_ids(Compensation),
|
||||||
self._write_warning(f" KOM: {KOM_log_entries_ids.count()} attached log entries")
|
self.get_all_log_entries_ids(EcoAccount),
|
||||||
OEK_log_entries_ids = self.get_all_log_entries_ids(EcoAccount)
|
self.get_all_log_entries_ids(Ema),
|
||||||
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=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
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
unattached_log_entries = all_log_entries.exclude(id__in=attached_log_entries_id)
|
||||||
|
|
||||||
num_entries = unattached_log_entries.count()
|
num_entries = unattached_log_entries.count()
|
||||||
if num_entries > 0:
|
if num_entries > 0:
|
||||||
self._write_error(f"Found {num_entries} log entries not attached to anything. Delete now...")
|
self._write_error(f"Found {num_entries} log entries not attached to anything. Delete now...")
|
||||||
@ -118,21 +108,14 @@ 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()
|
||||||
|
|
||||||
kom_action_ids = self.get_all_action_ids(Compensation)
|
compensation_action_ids = self.get_all_action_ids(Compensation)
|
||||||
self._write_warning(f" KOM: {kom_action_ids.count()} attached actions")
|
attached_action_ids = compensation_action_ids.union(
|
||||||
oek_action_ids = self.get_all_action_ids(EcoAccount)
|
self.get_all_action_ids(EcoAccount),
|
||||||
self._write_warning(f" OEK: {oek_action_ids.count()} attached actions")
|
self.get_all_action_ids(Ema),
|
||||||
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=kom_action_ids
|
|
||||||
).exclude(
|
|
||||||
id__in=oek_action_ids
|
|
||||||
).exclude(
|
|
||||||
id__in=ema_action_ids
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
unattached_actions = all_actions.exclude(id__in=attached_action_ids)
|
||||||
|
|
||||||
num_entries = unattached_actions.count()
|
num_entries = unattached_actions.count()
|
||||||
if num_entries > 0:
|
if num_entries > 0:
|
||||||
self._write_error(f"Found {num_entries} actions not attached to anything. Delete now...")
|
self._write_error(f"Found {num_entries} actions not attached to anything. Delete now...")
|
||||||
@ -142,7 +125,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:
|
||||||
@ -171,21 +154,14 @@ class Command(BaseKonovaCommand):
|
|||||||
self._write_warning("=== Sanitize deadlines ===")
|
self._write_warning("=== Sanitize deadlines ===")
|
||||||
all_deadlines = Deadline.objects.all()
|
all_deadlines = Deadline.objects.all()
|
||||||
|
|
||||||
kom_deadline_ids = self._get_all_deadline_ids(Compensation)
|
compensation_deadline_ids = self.get_all_deadline_ids(Compensation)
|
||||||
self._write_warning(f" KOM: {kom_deadline_ids.count()} attached deadlines")
|
attached_deadline_ids = compensation_deadline_ids.union(
|
||||||
oek_deadline_ids = self._get_all_deadline_ids(EcoAccount)
|
self.get_all_deadline_ids(EcoAccount),
|
||||||
self._write_warning(f" OEK: {kom_deadline_ids.count()} attached deadlines")
|
self.get_all_deadline_ids(Ema),
|
||||||
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=kom_deadline_ids
|
|
||||||
).exclude(
|
|
||||||
id__in=oek_deadline_ids
|
|
||||||
).exclude(
|
|
||||||
id__in=ema_deadline_ids
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
unattached_deadlines = all_deadlines.exclude(id__in=attached_deadline_ids)
|
||||||
|
|
||||||
num_entries = unattached_deadlines.count()
|
num_entries = unattached_deadlines.count()
|
||||||
if num_entries > 0:
|
if num_entries > 0:
|
||||||
self._write_error(f"Found {num_entries} deadlines not attached to anything. Delete now...")
|
self._write_error(f"Found {num_entries} deadlines not attached to anything. Delete now...")
|
||||||
@ -195,7 +171,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:
|
||||||
@ -224,25 +200,15 @@ class Command(BaseKonovaCommand):
|
|||||||
self._write_warning("=== Sanitize geometries ===")
|
self._write_warning("=== Sanitize geometries ===")
|
||||||
all_geometries = Geometry.objects.all()
|
all_geometries = Geometry.objects.all()
|
||||||
|
|
||||||
kom_geometry_ids = self._get_all_geometry_ids(Compensation)
|
compensation_geometry_ids = self.get_all_geometry_ids(Compensation)
|
||||||
self._write_warning(f" KOM: {kom_geometry_ids.count()} attached geometries")
|
attached_geometry_ids = compensation_geometry_ids.union(
|
||||||
eiv_geometry_ids = self._get_all_geometry_ids(Intervention)
|
self.get_all_geometry_ids(Intervention),
|
||||||
self._write_warning(f" EIV: {eiv_geometry_ids.count()} attached geometries")
|
self.get_all_geometry_ids(EcoAccount),
|
||||||
oek_geometry_ids = self._get_all_geometry_ids(EcoAccount)
|
self.get_all_geometry_ids(Ema),
|
||||||
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=kom_geometry_ids
|
|
||||||
).exclude(
|
|
||||||
id__in=eiv_geometry_ids
|
|
||||||
).exclude(
|
|
||||||
id__in=oek_geometry_ids
|
|
||||||
).exclude(
|
|
||||||
id__in=ema_geometry_ids
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
unattached_geometries = all_geometries.exclude(id__in=attached_geometry_ids)
|
||||||
|
|
||||||
num_entries = unattached_geometries.count()
|
num_entries = unattached_geometries.count()
|
||||||
if num_entries > 0:
|
if num_entries > 0:
|
||||||
self._write_error(f"Found {num_entries} geometries not attached to anything. Delete now...")
|
self._write_error(f"Found {num_entries} geometries not attached to anything. Delete now...")
|
||||||
@ -252,7 +218,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:
|
||||||
@ -288,19 +254,14 @@ 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)
|
||||||
kom_state_ids = self._get_all_state_ids(Compensation)
|
account_state_ids = self.get_all_state_ids(EcoAccount)
|
||||||
oek_state_ids = self._get_all_state_ids(EcoAccount)
|
ema_state_ids = self.get_all_state_ids(Ema)
|
||||||
ema_state_ids = self._get_all_state_ids(Ema)
|
attached_state_ids = compensation_state_ids.union(account_state_ids, ema_state_ids)
|
||||||
|
|
||||||
unattached_states = all_states.exclude(
|
unattached_states = all_states.exclude(
|
||||||
id__in=kom_state_ids
|
id__in=attached_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...")
|
||||||
|
@ -1,54 +0,0 @@
|
|||||||
"""
|
|
||||||
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)")
|
|
51
konova/management/commands/test_identifier_generating.py
Normal file
51
konova/management/commands/test_identifier_generating.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
"""
|
||||||
|
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")
|
54
konova/management/commands/update_all_parcels.py
Normal file
54
konova/management/commands/update_all_parcels.py
Normal file
@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
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()
|
@ -1,23 +0,0 @@
|
|||||||
# 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),
|
|
||||||
),
|
|
||||||
]
|
|
@ -1,17 +0,0 @@
|
|||||||
# 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,31 +8,19 @@ 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):
|
||||||
@ -121,14 +109,82 @@ class Geometry(BaseResource):
|
|||||||
objs += set_objs
|
objs += set_objs
|
||||||
return objs
|
return objs
|
||||||
|
|
||||||
def get_data_object(self):
|
@transaction.atomic
|
||||||
|
def update_parcels_wfs(self):
|
||||||
|
""" Updates underlying parcel information using the WFS of LVermGeo
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
Getter for the specific data object which is related to this geometry
|
from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup
|
||||||
"""
|
|
||||||
objs = self.get_data_objects()
|
if self.geom.empty:
|
||||||
assert (len(objs) <= 1)
|
# Nothing to do
|
||||||
result = objs.pop()
|
return
|
||||||
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
|
||||||
@ -136,152 +192,72 @@ 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 ", "")
|
||||||
|
district = District.objects.get_or_create(
|
||||||
# Get district (cache in dict)
|
key=result["kreisschl"],
|
||||||
try:
|
name=result["kreis"],
|
||||||
district = districts["kreisschl"]
|
)[0]
|
||||||
except KeyError:
|
municipal = Municipal.objects.get_or_create(
|
||||||
district = District.objects.get_or_create(
|
key=result["gmdschl"],
|
||||||
key=result["kreisschl"],
|
name=result["gemeinde"],
|
||||||
name=result["kreis"],
|
district=district,
|
||||||
)[0]
|
)[0]
|
||||||
districts[district.key] = district
|
parcel_group = ParcelGroup.objects.get_or_create(
|
||||||
|
key=result["gemaschl"],
|
||||||
# Get municipal (cache in dict)
|
name=result["gemarkung"],
|
||||||
try:
|
municipal=municipal,
|
||||||
municipal = municipals["gmdschl"]
|
)[0]
|
||||||
except KeyError:
|
|
||||||
municipal = Municipal.objects.get_or_create(
|
|
||||||
key=result["gmdschl"],
|
|
||||||
name=result["gemeinde"],
|
|
||||||
district=district,
|
|
||||||
)[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(
|
|
||||||
key=result["gemaschl"],
|
|
||||||
name=result["gemarkung"],
|
|
||||||
municipal=municipal,
|
|
||||||
)[0]
|
|
||||||
parcel_groups[parcel_group.key] = parcel_group
|
|
||||||
|
|
||||||
# Preprocess parcel data
|
|
||||||
flrstck_nnr = result['flstnrnen']
|
flrstck_nnr = result['flstnrnen']
|
||||||
match flrstck_nnr:
|
if not flrstck_nnr:
|
||||||
case "":
|
flrstck_nnr = None
|
||||||
flrstck_nnr = None
|
|
||||||
|
|
||||||
flrstck_zhlr = result['flstnrzae']
|
flrstck_zhlr = result['flstnrzae']
|
||||||
match flrstck_zhlr:
|
if not flrstck_zhlr:
|
||||||
case "":
|
flrstck_zhlr = None
|
||||||
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)
|
||||||
|
|
||||||
try:
|
# Update the linked parcels
|
||||||
# Try to fetch parcel from db. If it already exists, just update timestamp.
|
self.parcels.clear()
|
||||||
parcel_obj = Parcel.objects.get(
|
|
||||||
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 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)
|
|
||||||
|
|
||||||
# Create new parcels
|
|
||||||
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)
|
||||||
|
|
||||||
@transaction.atomic
|
# Set the calculated_on intermediate field, so this related data will be found on lookups
|
||||||
def _set_parcel_update_start_time(self):
|
intersections_without_ts = self.parcelintersection_set.filter(
|
||||||
"""
|
parcel__in=self.parcels.all(),
|
||||||
Sets the current time for the parcel calculation begin
|
calculated_on__isnull=True,
|
||||||
"""
|
)
|
||||||
self.parcel_update_start = timezone.now()
|
for entry in intersections_without_ts:
|
||||||
self.parcel_update_end = None
|
entry.calculated_on = _now
|
||||||
self.save()
|
ParcelIntersection.objects.bulk_update(
|
||||||
|
intersections_without_ts,
|
||||||
@transaction.atomic
|
["calculated_on"]
|
||||||
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
|
||||||
@ -289,7 +265,9 @@ class Geometry(BaseResource):
|
|||||||
Returns:
|
Returns:
|
||||||
parcels (QuerySet): The related parcels as queryset
|
parcels (QuerySet): The related parcels as queryset
|
||||||
"""
|
"""
|
||||||
parcels = self.parcels.prefetch_related(
|
parcels = self.parcels.filter(
|
||||||
|
parcelintersection__calculated_on__isnull=False,
|
||||||
|
).prefetch_related(
|
||||||
"district",
|
"district",
|
||||||
"municipal",
|
"municipal",
|
||||||
).order_by(
|
).order_by(
|
||||||
@ -314,6 +292,17 @@ 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
|
||||||
|
|
||||||
@ -348,41 +337,6 @@ 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,6 +672,17 @@ 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, transaction
|
from django.db import models
|
||||||
|
|
||||||
from konova.models import UuidModel
|
from konova.models import UuidModel
|
||||||
|
|
||||||
@ -158,51 +158,21 @@ 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,6 +18,7 @@ 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
|
||||||
@ -45,8 +46,4 @@ 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
|
|
||||||
|
78
konova/sso/sso.py
Normal file
78
konova/sso/sso.py
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
"""
|
||||||
|
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
|
@ -275,7 +275,4 @@ Similar to bootstraps 'shadow-lg'
|
|||||||
}
|
}
|
||||||
.tree-label.badge{
|
.tree-label.badge{
|
||||||
font-size: 90%;
|
font-size: 90%;
|
||||||
}
|
|
||||||
.alert{
|
|
||||||
margin-bottom: 0 !important;
|
|
||||||
}
|
}
|
@ -10,8 +10,6 @@ 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
|
||||||
|
|
||||||
@ -26,28 +24,32 @@ BASE_DIR = os.path.dirname(
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
env = environ.Env()
|
# Quick-start development settings - unsuitable for production
|
||||||
# Take environment variables from .env.dev file
|
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/
|
||||||
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 = env("SECRET_KEY")
|
SECRET_KEY = '5=9-)2)h$u9=!zrhia9=lj-2#cpcb8=#$7y+)l$5tto$3q(n_+'
|
||||||
|
|
||||||
# SECURITY WARNING: don't run with debug turned on in production!
|
# SECURITY WARNING: don't run with debug turned on in production!
|
||||||
DEBUG = env.bool("DEBUG", default=False)
|
DEBUG = True
|
||||||
|
|
||||||
ADMINS = [x.split(':') for x in env.list('ADMINS')]
|
ADMINS = [
|
||||||
|
('KSP-Servicestelle', 'ksp-servicestelle@sgdnord.rlp.de'),
|
||||||
|
]
|
||||||
|
|
||||||
ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
|
BASE_URL = "http://localhost:8001"
|
||||||
|
|
||||||
BASE_URL = env("BASE_URL")
|
ALLOWED_HOSTS = [
|
||||||
|
"127.0.0.1",
|
||||||
|
"localhost",
|
||||||
|
]
|
||||||
|
|
||||||
CSRF_TRUSTED_ORIGINS = [
|
CSRF_TRUSTED_ORIGINS = [
|
||||||
BASE_URL
|
"http://localhost", # not only host but schema (http/s) as well!
|
||||||
]
|
]
|
||||||
|
|
||||||
# Authentication settings
|
# Authentication settings
|
||||||
LOGIN_URL = "/oauth/login/"
|
LOGIN_URL = "/login/"
|
||||||
|
|
||||||
# Session settings
|
# Session settings
|
||||||
SESSION_COOKIE_AGE = 60 * 60 # 60 minutes
|
SESSION_COOKIE_AGE = 60 * 60 # 60 minutes
|
||||||
@ -66,6 +68,7 @@ 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',
|
||||||
@ -80,6 +83,10 @@ INSTALLED_APPS = [
|
|||||||
'analysis',
|
'analysis',
|
||||||
'api',
|
'api',
|
||||||
]
|
]
|
||||||
|
if DEBUG:
|
||||||
|
INSTALLED_APPS += [
|
||||||
|
'debug_toolbar',
|
||||||
|
]
|
||||||
|
|
||||||
MIDDLEWARE = [
|
MIDDLEWARE = [
|
||||||
'django.middleware.security.SecurityMiddleware',
|
'django.middleware.security.SecurityMiddleware',
|
||||||
@ -91,6 +98,10 @@ 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'
|
||||||
|
|
||||||
@ -120,11 +131,10 @@ WSGI_APPLICATION = 'konova.wsgi.application'
|
|||||||
DATABASES = {
|
DATABASES = {
|
||||||
'default': {
|
'default': {
|
||||||
'ENGINE': 'django.contrib.gis.db.backends.postgis',
|
'ENGINE': 'django.contrib.gis.db.backends.postgis',
|
||||||
'NAME': env("DB_NAME"),
|
'NAME': 'konova',
|
||||||
'USER': env("DB_USER"),
|
'USER': 'postgres',
|
||||||
'PASSWORD': env("DB_PASSWORD"),
|
'HOST': '127.0.0.1',
|
||||||
'HOST': env("DB_HOST"),
|
'PORT': '5432',
|
||||||
'PORT': env("DB_PORT"),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
|
||||||
@ -191,6 +201,28 @@ 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 !!!
|
||||||
@ -198,10 +230,13 @@ 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 = env("DEFAULT_FROM_EMAIL") # The default email address for the 'from' element
|
DEFAULT_FROM_EMAIL = "service@ksp.de" # 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 = env("SMTP_HOST")
|
EMAIL_HOST = "localhost"
|
||||||
EMAIL_REPLY_TO = env("REPLY_TO_ADDR")
|
EMAIL_REPLY_TO = "ksp-servicestelle@sgdnord.rlp.de"
|
||||||
EMAIL_PORT = env("SMTP_PORT")
|
SUPPORT_MAIL_RECIPIENT = EMAIL_REPLY_TO
|
||||||
|
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,7 +5,6 @@ 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
|
||||||
@ -29,6 +28,3 @@ LANIS_ZOOM_LUT = {
|
|||||||
1000: 30,
|
1000: 30,
|
||||||
500: 31,
|
500: 31,
|
||||||
}
|
}
|
||||||
|
|
||||||
MAP_PROXY_HOST_WHITELIST = env.list("MAP_PROXY_HOST_WHITELIST")
|
|
||||||
i = 0
|
|
@ -5,13 +5,12 @@ 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 = env("PROXY")
|
proxy = ""
|
||||||
PROXIES = {
|
PROXIES = {
|
||||||
"http": proxy,
|
"http": proxy,
|
||||||
"https": proxy,
|
"https": proxy,
|
||||||
}
|
}
|
||||||
|
|
||||||
GEOPORTAL_RLP_USER = env("GEOPORTAL_RLP_USER")
|
CLIENT_PROXY_AUTH_USER = "CHANGE_ME"
|
||||||
GEOPORTAL_RLP_PASSWORD = env("GEOPORTAL_RLP_PASSWORD")
|
CLIENT_PROXY_AUTH_PASSWORD = "CHANGE_ME"
|
@ -5,8 +5,7 @@ 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 = env("SCHNEIDER_BASE_URL")
|
base_url = "http://127.0.0.1:8002"
|
||||||
auth_header = env("SCHNEIDER_AUTH_HEADER")
|
auth_header = "auth"
|
||||||
auth_header_token = env("SCHNEIDER_AUTH_TOKEN")
|
auth_header_token = "CHANGE_ME"
|
||||||
|
@ -5,16 +5,9 @@ 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 = env("SSO_SERVER_BASE_URL")
|
SSO_SERVER_BASE = "http://127.0.0.1:8000/"
|
||||||
SSO_SERVER = f"{SSO_SERVER_BASE}sso/"
|
SSO_SERVER = f"{SSO_SERVER_BASE}sso/"
|
||||||
|
SSO_PRIVATE_KEY = "CHANGE_ME"
|
||||||
# OAuth settings
|
SSO_PUBLIC_KEY = "CHANGE_ME"
|
||||||
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")
|
|
12
konova/sub_settings/wfs_parcel_settings.py
Normal file
12
konova/sub_settings/wfs_parcel_settings.py
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
"""
|
||||||
|
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,12 +7,18 @@ 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
|
from konova.models import Geometry, ParcelIntersection
|
||||||
try:
|
try:
|
||||||
geom = Geometry.objects.get(id=geometry_id)
|
geom = Geometry.objects.get(id=geometry_id)
|
||||||
geom.parcels.clear()
|
objs = geom.parcelintersection_set.all()
|
||||||
geom.update_parcels()
|
for obj in objs:
|
||||||
|
obj.calculated_on = None
|
||||||
|
ParcelIntersection.objects.bulk_update(
|
||||||
|
objs,
|
||||||
|
["calculated_on"]
|
||||||
|
)
|
||||||
|
|
||||||
|
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|linebreaks}}
|
{{obj.comment}}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -146,6 +146,7 @@ class BaseTestCase(TestCase):
|
|||||||
geometry = Geometry.objects.create()
|
geometry = Geometry.objects.create()
|
||||||
# Finally create main object, holding the other objects
|
# Finally create main object, holding the other objects
|
||||||
intervention = Intervention.objects.create(
|
intervention = Intervention.objects.create(
|
||||||
|
identifier="TEST",
|
||||||
title="Test_title",
|
title="Test_title",
|
||||||
responsible=responsibility_data,
|
responsible=responsibility_data,
|
||||||
legal=legal_data,
|
legal=legal_data,
|
||||||
@ -173,6 +174,7 @@ class BaseTestCase(TestCase):
|
|||||||
geometry = Geometry.objects.create()
|
geometry = Geometry.objects.create()
|
||||||
# Finally create main object, holding the other objects
|
# Finally create main object, holding the other objects
|
||||||
compensation = Compensation.objects.create(
|
compensation = Compensation.objects.create(
|
||||||
|
identifier="TEST",
|
||||||
title="Test_title",
|
title="Test_title",
|
||||||
intervention=interv,
|
intervention=interv,
|
||||||
created=action,
|
created=action,
|
||||||
@ -198,8 +200,10 @@ class BaseTestCase(TestCase):
|
|||||||
responsible_data.handler = handler
|
responsible_data.handler = handler
|
||||||
responsible_data.save()
|
responsible_data.save()
|
||||||
|
|
||||||
|
identifier = EcoAccount().generate_new_identifier()
|
||||||
# Finally create main object, holding the other objects
|
# Finally create main object, holding the other objects
|
||||||
eco_account = EcoAccount.objects.create(
|
eco_account = EcoAccount.objects.create(
|
||||||
|
identifier=identifier,
|
||||||
title="Test_title",
|
title="Test_title",
|
||||||
deductable_surface=500,
|
deductable_surface=500,
|
||||||
legal=lega_data,
|
legal=lega_data,
|
||||||
@ -226,6 +230,7 @@ class BaseTestCase(TestCase):
|
|||||||
responsible_data.save()
|
responsible_data.save()
|
||||||
# Finally create main object, holding the other objects
|
# Finally create main object, holding the other objects
|
||||||
ema = Ema.objects.create(
|
ema = Ema.objects.create(
|
||||||
|
identifier="TEST",
|
||||||
title="Test_title",
|
title="Test_title",
|
||||||
responsible=responsible_data,
|
responsible=responsible_data,
|
||||||
created=action,
|
created=action,
|
||||||
@ -469,7 +474,7 @@ class BaseTestCase(TestCase):
|
|||||||
eco_account.save()
|
eco_account.save()
|
||||||
return eco_account
|
return eco_account
|
||||||
|
|
||||||
def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon, tolerance = 0.001):
|
def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon):
|
||||||
""" Assert for geometries to be equal
|
""" Assert for geometries to be equal
|
||||||
|
|
||||||
Transforms the geometries to matching srids before checking
|
Transforms the geometries to matching srids before checking
|
||||||
@ -486,6 +491,7 @@ class BaseTestCase(TestCase):
|
|||||||
self.assertTrue(True)
|
self.assertTrue(True)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
tolerance = 0.001
|
||||||
if geom1.srid != geom2.srid:
|
if geom1.srid != geom2.srid:
|
||||||
# Due to prior possible transformation of any of these geometries, we need to make sure there exists a
|
# Due to prior possible transformation of any of these geometries, we need to make sure there exists a
|
||||||
# transformation from one coordinate system into the other, which is valid
|
# transformation from one coordinate system into the other, which is valid
|
||||||
@ -509,7 +515,7 @@ class BaseViewTestCase(BaseTestCase):
|
|||||||
|
|
||||||
def setUp(self) -> None:
|
def setUp(self) -> None:
|
||||||
super().setUp()
|
super().setUp()
|
||||||
self.login_url = reverse("oauth-login")
|
self.login_url = reverse("simple-sso-login")
|
||||||
|
|
||||||
def assert_url_success(self, client: Client, urls: list):
|
def assert_url_success(self, client: Client, urls: list):
|
||||||
""" Assert for all given urls a direct 200 response
|
""" Assert for all given urls a direct 200 response
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user