Compare commits
148 Commits
1.4.1
...
490_View_r
| Author | SHA1 | Date | |
|---|---|---|---|
| 88058d7caf | |||
| 0e6f8d5b55 | |||
| 3966521cd4 | |||
| e70a8b51d1 | |||
| 02dc0d0a59 | |||
| 0b84d418db | |||
| 6aad76866f | |||
| 1af807deae | |||
| a2bda8d230 | |||
| e4c459f92e | |||
| 2da6f1dc6f | |||
| 72914bab9d | |||
| fdf3adf5ae | |||
| 4c4d64cc3d | |||
| fbde03caec | |||
| 43eb598d3f | |||
| b7fac0ae03 | |||
| 447ba942b5 | |||
| 6df47f1615 | |||
| e25d549a97 | |||
| 5e65b8f4dc | |||
| 22cddb9902 | |||
| c986bd0b92 | |||
| 2c60d86177 | |||
| b7792ececc | |||
| 210f3fcafa | |||
| e7d67560f2 | |||
| b5e991fb95 | |||
| d3a555d406 | |||
| 9a374f50de | |||
| ce63dd30bc | |||
| e5db7f6b13 | |||
| 442f3ceb37 | |||
| cd2949fc03 | |||
| 7bcd32fd7a | |||
| 97f1882698 | |||
| 55cc8eb1f3 | |||
| d6cabd5f6c | |||
| 63a824f9d9 | |||
| a12c2fb57e | |||
| 23bc79ee3b | |||
| 07bac26a58 | |||
| d01816cf71 | |||
| f543dfc1cb | |||
| 62fc019127 | |||
| c01518b675 | |||
| f57306eb72 | |||
| c2b12649b0 | |||
| 6fe67a8fbf | |||
| b38a266bea | |||
| 15f86e866b | |||
| c85b136f0a | |||
| faf8aed777 | |||
| 94c498866f | |||
| 616965c890 | |||
| e39c7eb51f | |||
| 19bd408fbd | |||
| 7bfe6a37f8 | |||
| 9b63307f01 | |||
| 123a470006 | |||
| 5a0c5285e7 | |||
| 3a01eaff92 | |||
| 2af91aa178 | |||
| 53d0af89ac | |||
| 7b5c1f0d97 | |||
| ef076c0b3b | |||
| 72a5075f3b | |||
| d677ac6b5a | |||
| 9149e4cbd3 | |||
| 1c24cbea26 | |||
|
|
bc9cc09372 | ||
| 24518465f3 | |||
| 457548da4d | |||
| fa89bbba99 | |||
| 78eb711057 | |||
| 6ff67d12c9 | |||
| 3c1cbcd0bd | |||
| 416ad8478c | |||
| 6b28c4ec15 | |||
| 46a2a4ff46 | |||
| 90e5cf5b36 | |||
| 50f46e319c | |||
| e2ea087c4e | |||
| a6e43b044b | |||
| be0d261e81 | |||
| 62e1b046c3 | |||
| 669a12410f | |||
| dd77e6c16e | |||
| 33774ce557 | |||
| dc3dc99b3d | |||
| 315f9de958 | |||
| 0726c15086 | |||
| 2492a8abe8 | |||
| dbc5cba5d7 | |||
| c8948ddaea | |||
| 5039da28aa | |||
| 4567339570 | |||
| 768849e646 | |||
| ebf10645fc | |||
| df241747cf | |||
| 94e9035e10 | |||
| d4d39689cc | |||
| 2fde3f0fa3 | |||
| b62c2e92c9 | |||
| 1c0fb801e6 | |||
| a1acff5e90 | |||
| 25a92f59aa | |||
| 884db6f014 | |||
| 59b7f3c69a | |||
| 0446d50438 | |||
| 12f78c85bf | |||
| 78485a4506 | |||
| 21a5c84b18 | |||
| a93f509d51 | |||
| 38967da201 | |||
| 60d749db2d | |||
| dff577309e | |||
| ea590d0868 | |||
| e09c15bd51 | |||
| c3019f83fd | |||
| 93a71a7055 | |||
| 35b1409359 | |||
| c9aeb393b5 | |||
| 6df46e7642 | |||
| fe366bc568 | |||
| a9f04a28c1 | |||
| 8c9f4888dd | |||
| 5c727b2eaa | |||
| 76b2a78fe2 | |||
| 86db08fca0 | |||
| fe1dce6440 | |||
| a5e6f5a1db | |||
| 78e9cbab71 | |||
| 572348f9f1 | |||
| 8ff3cb9adc | |||
| f135008447 | |||
| 94b7f3ad70 | |||
| d69bab36da | |||
| fa86cc142f | |||
| 6523891703 | |||
| 18f590f4a6 | |||
| b441518334 | |||
| 1a80912960 | |||
| 04dc7fcd30 | |||
| 09546212b9 | |||
| b1cd7dee40 | |||
| c772e1de06 | |||
| 4332a750d1 |
48
.env.sample
Normal file
48
.env.sample
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
# General
|
||||||
|
SECRET_KEY=CHANGE_ME
|
||||||
|
DEBUG=True
|
||||||
|
ALLOWED_HOSTS=127.0.0.1,localhost,example.org
|
||||||
|
BASE_URL=http://localhost:8002
|
||||||
|
ADMINS=Admin1:mail@example.org,Admin2:mail2@example.org
|
||||||
|
|
||||||
|
# Database
|
||||||
|
DB_USER=postgres
|
||||||
|
DB_PASSWORD=
|
||||||
|
DB_NAME=konova
|
||||||
|
DB_HOST=127.0.0.1
|
||||||
|
DB_PORT=5432
|
||||||
|
|
||||||
|
# Redis (for celery)
|
||||||
|
REDIS_HOST=CHANGE_ME
|
||||||
|
REDIS_PORT=CHANGE_ME
|
||||||
|
|
||||||
|
# E-Mail
|
||||||
|
SMTP_HOST=localhost
|
||||||
|
SMTP_PORT=25
|
||||||
|
REPLY_TO_ADDR=ksp-servicestelle@sgdnord.rlp.de
|
||||||
|
DEFAULT_FROM_EMAIL=service@ksp.de
|
||||||
|
|
||||||
|
# Proxy
|
||||||
|
PROXY=CHANGE_ME
|
||||||
|
MAP_PROXY_HOST_WHITELIST=CHANGE_ME_1,CHANGE_ME_2
|
||||||
|
GEOPORTAL_RLP_USER=CHANGE_ME
|
||||||
|
GEOPORTAL_RLP_PASSWORD=CHANGE_ME
|
||||||
|
|
||||||
|
# Schneider
|
||||||
|
SCHNEIDER_BASE_URL=https://schneider.naturschutz.rlp.de
|
||||||
|
SCHNEIDER_AUTH_TOKEN=CHANGE_ME
|
||||||
|
SCHNEIDER_AUTH_HEADER=auth
|
||||||
|
|
||||||
|
# SSO
|
||||||
|
SSO_SERVER_BASE_URL=https://login.naturschutz.rlp.de
|
||||||
|
OAUTH_CODE_VERIFIER=CHANGE_ME
|
||||||
|
OAUTH_CLIENT_ID=CHANGE_ME
|
||||||
|
OAUTH_CLIENT_SECRET=CHANGE_ME
|
||||||
|
PROPAGATION_SECRET=CHANGE_ME
|
||||||
|
|
||||||
|
# RabbitMQ
|
||||||
|
## For connections to EGON
|
||||||
|
EGON_RABBITMQ_HOST=CHANGE_ME
|
||||||
|
EGON_RABBITMQ_PORT=CHANGE_ME
|
||||||
|
EGON_RABBITMQ_USER=CHANGE_ME
|
||||||
|
EGON_RABBITMQ_PW=CHANGE_ME
|
||||||
1
.gitignore
vendored
1
.gitignore
vendored
@@ -3,3 +3,4 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
/.coverage
|
/.coverage
|
||||||
/htmlcov/
|
/htmlcov/
|
||||||
|
/.env
|
||||||
|
|||||||
15
api/admin.py
15
api/admin.py
@@ -1,6 +1,6 @@
|
|||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
|
||||||
from api.models.token import APIUserToken
|
from api.models.token import APIUserToken, OAuthToken
|
||||||
|
|
||||||
|
|
||||||
class APITokenAdmin(admin.ModelAdmin):
|
class APITokenAdmin(admin.ModelAdmin):
|
||||||
@@ -17,4 +17,17 @@ class APITokenAdmin(admin.ModelAdmin):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthTokenAdmin(admin.ModelAdmin):
|
||||||
|
list_display = [
|
||||||
|
"access_token",
|
||||||
|
"refresh_token",
|
||||||
|
"expires_on",
|
||||||
|
]
|
||||||
|
search_fields = [
|
||||||
|
"access_token",
|
||||||
|
"refresh_token",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(APIUserToken, APITokenAdmin)
|
admin.site.register(APIUserToken, APITokenAdmin)
|
||||||
|
admin.site.register(OAuthToken, OAuthTokenAdmin)
|
||||||
|
|||||||
26
api/migrations/0003_oauthtoken.py
Normal file
26
api/migrations/0003_oauthtoken.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
# Generated by Django 5.0.4 on 2024-04-30 07:20
|
||||||
|
|
||||||
|
import uuid
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('api', '0002_alter_apiusertoken_valid_until'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name='OAuthToken',
|
||||||
|
fields=[
|
||||||
|
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
|
||||||
|
('access_token', models.CharField(db_comment='OAuth access token', max_length=255)),
|
||||||
|
('refresh_token', models.CharField(db_comment='OAuth refresh token', max_length=255)),
|
||||||
|
('expires_on', models.DateTimeField(db_comment='When the token will be expired')),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
'abstract': False,
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -1,7 +1,14 @@
|
|||||||
|
import json
|
||||||
|
from datetime import timedelta
|
||||||
|
|
||||||
|
import requests
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.utils.timezone import now
|
||||||
|
|
||||||
|
from konova.models import UuidModel
|
||||||
|
from konova.sub_settings.sso_settings import OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, SSO_SERVER_BASE
|
||||||
from konova.utils.generators import generate_token
|
from konova.utils.generators import generate_token
|
||||||
|
|
||||||
|
|
||||||
@@ -44,5 +51,129 @@ class APIUserToken(models.Model):
|
|||||||
if token_obj.valid_until is not None and token_obj.valid_until < _today:
|
if token_obj.valid_until is not None and token_obj.valid_until < _today:
|
||||||
raise PermissionError("Token validity expired")
|
raise PermissionError("Token validity expired")
|
||||||
except ObjectDoesNotExist:
|
except ObjectDoesNotExist:
|
||||||
raise PermissionError("Credentials invalid")
|
raise PermissionError("Token unknown")
|
||||||
return token_obj.user
|
return token_obj.user
|
||||||
|
|
||||||
|
|
||||||
|
class OAuthToken(UuidModel):
|
||||||
|
access_token = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
|
db_comment="OAuth access token"
|
||||||
|
)
|
||||||
|
refresh_token = models.CharField(
|
||||||
|
max_length=255,
|
||||||
|
blank=False,
|
||||||
|
null=False,
|
||||||
|
db_comment="OAuth refresh token"
|
||||||
|
)
|
||||||
|
expires_on = models.DateTimeField(
|
||||||
|
db_comment="When the token will be expired"
|
||||||
|
)
|
||||||
|
|
||||||
|
ASSUMED_LATENCY = 1000 # assumed latency between creation and receiving of an access token
|
||||||
|
|
||||||
|
def __str__(self):
|
||||||
|
return str(self.access_token)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def from_access_token_response(access_token_data: str, received_on):
|
||||||
|
"""
|
||||||
|
Creates an OAuthToken based on retrieved access token data (OAuth2.0 specification)
|
||||||
|
|
||||||
|
Args:
|
||||||
|
access_token_data (str): OAuth2.0 response data
|
||||||
|
received_on (): Timestamp when the response has been received
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
oauth_token = OAuthToken()
|
||||||
|
data = json.loads(access_token_data)
|
||||||
|
|
||||||
|
oauth_token.access_token = data.get("access_token")
|
||||||
|
oauth_token.refresh_token = data.get("refresh_token")
|
||||||
|
|
||||||
|
expires_on = received_on + timedelta(
|
||||||
|
seconds=(data.get("expires_in") + OAuthToken.ASSUMED_LATENCY)
|
||||||
|
)
|
||||||
|
oauth_token.expires_on = expires_on
|
||||||
|
|
||||||
|
return oauth_token
|
||||||
|
|
||||||
|
def refresh(self):
|
||||||
|
url = f"{SSO_SERVER_BASE}o/token/"
|
||||||
|
params = {
|
||||||
|
"grant_type": "refresh_token",
|
||||||
|
"refresh_token": self.refresh_token,
|
||||||
|
"client_id": OAUTH_CLIENT_ID,
|
||||||
|
"client_secret": OAUTH_CLIENT_SECRET
|
||||||
|
}
|
||||||
|
response = requests.post(
|
||||||
|
url,
|
||||||
|
params
|
||||||
|
)
|
||||||
|
_now = now()
|
||||||
|
is_response_invalid = response.status_code != 200
|
||||||
|
if is_response_invalid:
|
||||||
|
raise RuntimeError(f"Refreshing token not possible: {response.status_code}")
|
||||||
|
|
||||||
|
response_content = response.content.decode("utf-8")
|
||||||
|
response_content = json.loads(response_content)
|
||||||
|
|
||||||
|
access_token = response_content.get("access_token")
|
||||||
|
refresh_token = response_content.get("refresh_token")
|
||||||
|
expires_in = response_content.get("expires")
|
||||||
|
|
||||||
|
self.access_token = access_token
|
||||||
|
self.refresh_token = refresh_token
|
||||||
|
self.expires_in = expires_in
|
||||||
|
self.save()
|
||||||
|
|
||||||
|
return self
|
||||||
|
|
||||||
|
def update_and_get_user(self):
|
||||||
|
from user.models import User
|
||||||
|
url = f"{SSO_SERVER_BASE}users/oauth/data/"
|
||||||
|
|
||||||
|
access_token = self.access_token
|
||||||
|
response = requests.get(
|
||||||
|
url,
|
||||||
|
headers={
|
||||||
|
"Authorization": f"Bearer {access_token}",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
is_response_code_invalid = response.status_code != 200
|
||||||
|
if is_response_code_invalid:
|
||||||
|
raise RuntimeError(f"OAuth user data fetching unsuccessful: {response.status_code}")
|
||||||
|
|
||||||
|
response_content = response.content.decode("utf-8")
|
||||||
|
response_content = json.loads(response_content)
|
||||||
|
user = User.oauth_update_user(response_content)
|
||||||
|
|
||||||
|
return user
|
||||||
|
|
||||||
|
def revoke(self) -> int:
|
||||||
|
""" Revokes the OAuth2 token of the user
|
||||||
|
|
||||||
|
(/o/revoke_token/ indeed removes the corresponding access token on provider side and invalidates the
|
||||||
|
submitted refresh token in one step)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
revocation_status_code (int): HTTP status code for revocation of refresh_token
|
||||||
|
"""
|
||||||
|
revoke_url = f"{SSO_SERVER_BASE}o/revoke_token/"
|
||||||
|
token = self.refresh_token
|
||||||
|
revocation_status_code = requests.post(
|
||||||
|
revoke_url,
|
||||||
|
data={
|
||||||
|
'token': token,
|
||||||
|
'token_type_hint': "refresh_token",
|
||||||
|
},
|
||||||
|
auth=(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET),
|
||||||
|
).status_code
|
||||||
|
|
||||||
|
return revocation_status_code
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ from django.contrib.gis import geos
|
|||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
|
||||||
from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase
|
from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase
|
||||||
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
|
from konova.models import Geometry
|
||||||
|
|
||||||
|
|
||||||
class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
||||||
@@ -64,7 +64,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
|||||||
|
|
||||||
put_props = put_body["properties"]
|
put_props = put_body["properties"]
|
||||||
put_geom = geos.fromstr(json.dumps(put_body))
|
put_geom = geos.fromstr(json.dumps(put_body))
|
||||||
put_geom.transform(DEFAULT_SRID_RLP)
|
put_geom = Geometry.cast_to_rlp_srid(put_geom)
|
||||||
self.assertEqual(put_geom, self.intervention.geometry.geom)
|
self.assertEqual(put_geom, self.intervention.geometry.geom)
|
||||||
self.assertEqual(put_props["title"], self.intervention.title)
|
self.assertEqual(put_props["title"], self.intervention.title)
|
||||||
self.assertNotEqual(modified_on, self.intervention.modified)
|
self.assertNotEqual(modified_on, self.intervention.modified)
|
||||||
@@ -94,7 +94,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
|||||||
|
|
||||||
put_props = put_body["properties"]
|
put_props = put_body["properties"]
|
||||||
put_geom = geos.fromstr(json.dumps(put_body))
|
put_geom = geos.fromstr(json.dumps(put_body))
|
||||||
put_geom.transform(DEFAULT_SRID_RLP)
|
put_geom = Geometry.cast_to_rlp_srid(put_geom)
|
||||||
self.assertEqual(put_geom, self.compensation.geometry.geom)
|
self.assertEqual(put_geom, self.compensation.geometry.geom)
|
||||||
self.assertEqual(put_props["title"], self.compensation.title)
|
self.assertEqual(put_props["title"], self.compensation.title)
|
||||||
self.assertNotEqual(modified_on, self.compensation.modified)
|
self.assertNotEqual(modified_on, self.compensation.modified)
|
||||||
@@ -124,7 +124,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
|||||||
|
|
||||||
put_props = put_body["properties"]
|
put_props = put_body["properties"]
|
||||||
put_geom = geos.fromstr(json.dumps(put_body))
|
put_geom = geos.fromstr(json.dumps(put_body))
|
||||||
put_geom.transform(DEFAULT_SRID_RLP)
|
put_geom = Geometry.cast_to_rlp_srid(put_geom)
|
||||||
self.assertEqual(put_geom, self.eco_account.geometry.geom)
|
self.assertEqual(put_geom, self.eco_account.geometry.geom)
|
||||||
self.assertEqual(put_props["title"], self.eco_account.title)
|
self.assertEqual(put_props["title"], self.eco_account.title)
|
||||||
self.assertNotEqual(modified_on, self.eco_account.modified)
|
self.assertNotEqual(modified_on, self.eco_account.modified)
|
||||||
@@ -156,7 +156,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
|
|||||||
|
|
||||||
put_props = put_body["properties"]
|
put_props = put_body["properties"]
|
||||||
put_geom = geos.fromstr(json.dumps(put_body))
|
put_geom = geos.fromstr(json.dumps(put_body))
|
||||||
put_geom.transform(DEFAULT_SRID_RLP)
|
put_geom = Geometry.cast_to_rlp_srid(put_geom)
|
||||||
self.assertEqual(put_geom, self.ema.geometry.geom)
|
self.assertEqual(put_geom, self.ema.geometry.geom)
|
||||||
self.assertEqual(put_props["title"], self.ema.title)
|
self.assertEqual(put_props["title"], self.ema.title)
|
||||||
self.assertNotEqual(modified_on, self.ema.modified)
|
self.assertNotEqual(modified_on, self.ema.modified)
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ 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.models import Geometry
|
||||||
from konova.utils.message_templates import DATA_UNSHARED
|
from konova.utils.message_templates import DATA_UNSHARED
|
||||||
|
|
||||||
|
|
||||||
@@ -32,8 +33,8 @@ class AbstractModelAPISerializer:
|
|||||||
self.lookup = {
|
self.lookup = {
|
||||||
"id": None, # must be set
|
"id": None, # must be set
|
||||||
"deleted__isnull": True,
|
"deleted__isnull": True,
|
||||||
"users__in": [], # must be set
|
|
||||||
}
|
}
|
||||||
|
self.shared_lookup = Q() # must be set, so user or team based share will be used properly
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
@@ -76,7 +77,11 @@ class AbstractModelAPISerializer:
|
|||||||
else:
|
else:
|
||||||
# Return certain object
|
# Return certain object
|
||||||
self.lookup["id"] = _id
|
self.lookup["id"] = _id
|
||||||
self.lookup["users__in"] = [user]
|
|
||||||
|
self.shared_lookup = Q(
|
||||||
|
Q(users__in=[user]) |
|
||||||
|
Q(teams__in=list(user.shared_teams))
|
||||||
|
)
|
||||||
|
|
||||||
def fetch_and_serialize(self):
|
def fetch_and_serialize(self):
|
||||||
""" Serializes the model entry according to the given lookup data
|
""" Serializes the model entry according to the given lookup data
|
||||||
@@ -86,7 +91,13 @@ class AbstractModelAPISerializer:
|
|||||||
Returns:
|
Returns:
|
||||||
serialized_data (dict)
|
serialized_data (dict)
|
||||||
"""
|
"""
|
||||||
entries = self.model.objects.filter(**self.lookup).order_by("id")
|
entries = self.model.objects.filter(
|
||||||
|
**self.lookup
|
||||||
|
).filter(
|
||||||
|
self.shared_lookup
|
||||||
|
).order_by(
|
||||||
|
"id"
|
||||||
|
).distinct()
|
||||||
self.paginator = Paginator(entries, self.rpp)
|
self.paginator = Paginator(entries, self.rpp)
|
||||||
requested_entries = self.paginator.page(self.page_number)
|
requested_entries = self.paginator.page(self.page_number)
|
||||||
|
|
||||||
@@ -134,8 +145,8 @@ class AbstractModelAPISerializer:
|
|||||||
if isinstance(geojson, dict):
|
if isinstance(geojson, dict):
|
||||||
geojson = json.dumps(geojson)
|
geojson = json.dumps(geojson)
|
||||||
geometry = geos.fromstr(geojson)
|
geometry = geos.fromstr(geojson)
|
||||||
if geometry.srid != DEFAULT_SRID_RLP:
|
geometry = Geometry.cast_to_rlp_srid(geometry)
|
||||||
geometry.transform(DEFAULT_SRID_RLP)
|
geometry = Geometry.cast_to_multipolygon(geometry)
|
||||||
return geometry
|
return geometry
|
||||||
|
|
||||||
def _get_obj_from_db(self, id, user):
|
def _get_obj_from_db(self, id, user):
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Created on: 24.01.22
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin
|
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin
|
||||||
from compensation.models import Compensation
|
from compensation.models import Compensation
|
||||||
@@ -21,8 +22,10 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
|
|||||||
|
|
||||||
def prepare_lookup(self, id, user):
|
def prepare_lookup(self, id, user):
|
||||||
super().prepare_lookup(id, user)
|
super().prepare_lookup(id, user)
|
||||||
del self.lookup["users__in"]
|
self.shared_lookup = Q(
|
||||||
self.lookup["intervention__users__in"] = [user]
|
Q(intervention__users__in=[user]) |
|
||||||
|
Q(intervention__teams__in=user.shared_teams)
|
||||||
|
)
|
||||||
|
|
||||||
def intervention_to_json(self, entry):
|
def intervention_to_json(self, entry):
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ Created on: 28.01.22
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
from api.utils.serializer.v1.serializer import DeductableAPISerializerV1Mixin, AbstractModelAPISerializerV1
|
from api.utils.serializer.v1.serializer import DeductableAPISerializerV1Mixin, AbstractModelAPISerializerV1
|
||||||
from compensation.models import EcoAccountDeduction, EcoAccount
|
from compensation.models import EcoAccountDeduction, EcoAccount
|
||||||
@@ -28,9 +29,11 @@ class DeductionAPISerializerV1(AbstractModelAPISerializerV1,
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
super().prepare_lookup(_id, user)
|
super().prepare_lookup(_id, user)
|
||||||
del self.lookup["users__in"]
|
|
||||||
del self.lookup["deleted__isnull"]
|
del self.lookup["deleted__isnull"]
|
||||||
self.lookup["intervention__users__in"] = [user]
|
self.shared_lookup = Q(
|
||||||
|
Q(intervention__users__in=[user]) |
|
||||||
|
Q(intervention__teams__in=user.shared_teams)
|
||||||
|
)
|
||||||
|
|
||||||
def _model_to_geo_json(self, entry):
|
def _model_to_geo_json(self, entry):
|
||||||
""" Adds the basic data
|
""" Adds the basic data
|
||||||
|
|||||||
@@ -16,7 +16,8 @@ from api.utils.serializer.serializer import AbstractModelAPISerializer
|
|||||||
from codelist.models import KonovaCode
|
from codelist.models import KonovaCode
|
||||||
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \
|
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \
|
||||||
CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \
|
CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \
|
||||||
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_HANDLER_ID
|
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_HANDLER_ID, \
|
||||||
|
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
from compensation.models import CompensationAction, UnitChoices, CompensationState
|
from compensation.models import CompensationAction, UnitChoices, CompensationState
|
||||||
from intervention.models import Responsibility, Legal, Handler
|
from intervention.models import Responsibility, Legal, Handler
|
||||||
from konova.models import Deadline, DeadlineType
|
from konova.models import Deadline, DeadlineType
|
||||||
@@ -347,7 +348,7 @@ class AbstractCompensationAPISerializerV1Mixin:
|
|||||||
try:
|
try:
|
||||||
biotope_type = entry["biotope"]
|
biotope_type = entry["biotope"]
|
||||||
biotope_details = [
|
biotope_details = [
|
||||||
self._konova_code_from_json(e, CODELIST_BIOTOPES_EXTRA_CODES_ID) for e in entry["biotope_details"]
|
self._konova_code_from_json(e, CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID) for e in entry["biotope_details"]
|
||||||
]
|
]
|
||||||
surface = float(entry["surface"])
|
surface = float(entry["surface"])
|
||||||
except KeyError:
|
except KeyError:
|
||||||
|
|||||||
@@ -23,11 +23,6 @@ class AbstractAPIViewV1(AbstractAPIView):
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.lookup = {
|
|
||||||
"id": None, # must be set in subclasses
|
|
||||||
"deleted__isnull": True,
|
|
||||||
"users__in": [], # must be set in subclasses
|
|
||||||
}
|
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.serializer = self.serializer()
|
self.serializer = self.serializer()
|
||||||
|
|
||||||
|
|||||||
@@ -50,14 +50,19 @@ class AbstractAPIView(View):
|
|||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
try:
|
try:
|
||||||
# Fetch the proper user from the given request header token
|
# Fetch the proper user from the given request header token
|
||||||
ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
|
token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
|
||||||
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
|
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
|
||||||
token_user = APIUserToken.get_user_from_token(ksp_token)
|
|
||||||
|
|
||||||
if ksp_user != token_user.username:
|
if not token and not ksp_user:
|
||||||
|
bearer_token = request.headers.get("authorization", None)
|
||||||
|
if not bearer_token:
|
||||||
|
raise PermissionError("No token provided")
|
||||||
|
token = bearer_token.split(" ")[1]
|
||||||
|
|
||||||
|
token_user = APIUserToken.get_user_from_token(token)
|
||||||
|
if ksp_user and ksp_user != token_user.username:
|
||||||
raise PermissionError(f"Invalid token for {ksp_user}")
|
raise PermissionError(f"Invalid token for {ksp_user}")
|
||||||
else:
|
self.user = token_user
|
||||||
self.user = token_user
|
|
||||||
|
|
||||||
request.user = self.user
|
request.user = self.user
|
||||||
if not self.user.is_default_user():
|
if not self.user.is_default_user():
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import collections
|
|||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
|
|
||||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
|
from codelist.settings import CODELIST_BIOTOPES_ID, \
|
||||||
|
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
from codelist.autocomplete.base import KonovaCodeAutocomplete
|
from codelist.autocomplete.base import KonovaCodeAutocomplete
|
||||||
from konova.utils.message_templates import UNGROUPED
|
from konova.utils.message_templates import UNGROUPED
|
||||||
|
|
||||||
@@ -84,11 +85,11 @@ class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
|
|||||||
Due to limitations of the django dal package, we need to subclass for each code list
|
Due to limitations of the django dal package, we need to subclass for each code list
|
||||||
"""
|
"""
|
||||||
group_by_related = "parent"
|
group_by_related = "parent"
|
||||||
related_field_name = "long_name"
|
related_field_name = "short_name"
|
||||||
paginate_by = 200
|
paginate_by = 200
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.c = CODELIST_BIOTOPES_EXTRA_CODES_ID
|
self.c = CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
def order_by(self, qs):
|
def order_by(self, qs):
|
||||||
@@ -103,8 +104,11 @@ class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
|
|||||||
qs (QuerySet): The ordered queryset
|
qs (QuerySet): The ordered queryset
|
||||||
"""
|
"""
|
||||||
return qs.order_by(
|
return qs.order_by(
|
||||||
"long_name",
|
"short_name",
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_result_label(self, result):
|
def get_result_label(self, result):
|
||||||
return f"{result.long_name} ({result.short_name})"
|
return f"{result.long_name} ({result.short_name})"
|
||||||
|
|
||||||
|
def get_selected_result_label(self, result):
|
||||||
|
return f"{result.parent.short_name} > {result.long_name} ({result.short_name})"
|
||||||
@@ -13,7 +13,7 @@ from codelist.settings import CODELIST_INTERVENTION_HANDLER_ID, CODELIST_CONSERV
|
|||||||
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, CODELIST_HANDLER_ID, \
|
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, CODELIST_HANDLER_ID, \
|
||||||
CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID, \
|
CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID, \
|
||||||
CODELIST_BASE_URL, CODELIST_PROCESS_TYPE_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
|
CODELIST_BASE_URL, CODELIST_PROCESS_TYPE_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
|
||||||
CODELIST_COMPENSATION_ACTION_DETAIL_ID
|
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
from konova.management.commands.setup import BaseKonovaCommand
|
from konova.management.commands.setup import BaseKonovaCommand
|
||||||
from konova.settings import PROXIES
|
from konova.settings import PROXIES
|
||||||
|
|
||||||
@@ -34,6 +34,7 @@ class Command(BaseKonovaCommand):
|
|||||||
CODELIST_REGISTRATION_OFFICE_ID,
|
CODELIST_REGISTRATION_OFFICE_ID,
|
||||||
CODELIST_BIOTOPES_ID,
|
CODELIST_BIOTOPES_ID,
|
||||||
CODELIST_BIOTOPES_EXTRA_CODES_ID,
|
CODELIST_BIOTOPES_EXTRA_CODES_ID,
|
||||||
|
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID,
|
||||||
CODELIST_LAW_ID,
|
CODELIST_LAW_ID,
|
||||||
CODELIST_HANDLER_ID,
|
CODELIST_HANDLER_ID,
|
||||||
CODELIST_COMPENSATION_ACTION_ID,
|
CODELIST_COMPENSATION_ACTION_ID,
|
||||||
@@ -55,7 +56,7 @@ class Command(BaseKonovaCommand):
|
|||||||
content = result.content.decode("utf-8")
|
content = result.content.decode("utf-8")
|
||||||
root = etree.fromstring(content)
|
root = etree.fromstring(content)
|
||||||
items = root.findall("item")
|
items = root.findall("item")
|
||||||
self._write_warning("Found {} codes. Process now...".format(len(items)))
|
self._write_warning(" Found {} codes. Process now...".format(len(items)))
|
||||||
|
|
||||||
code_list = KonovaCodeList.objects.get_or_create(
|
code_list = KonovaCodeList.objects.get_or_create(
|
||||||
id=list_id,
|
id=list_id,
|
||||||
@@ -74,15 +75,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
|
short_name = element.find("shortname").text or ""
|
||||||
long_name = element.find("longname").text
|
long_name = element.find("longname").text or ""
|
||||||
is_archived = bool_map.get((element.find("archive").text.lower()), False)
|
is_archived = bool_map.get((element.find("archive").text.lower()), False)
|
||||||
|
|
||||||
code = KonovaCode.objects.get_or_create(
|
code = KonovaCode.objects.get_or_create(
|
||||||
|
|||||||
60
codelist/migrations/0002_migrate_975_to_288.py
Normal file
60
codelist/migrations/0002_migrate_975_to_288.py
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
# Generated by Django 5.0.7 on 2024-08-06 13:40
|
||||||
|
|
||||||
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
|
from django.db import migrations
|
||||||
|
from django.db.models import Q
|
||||||
|
|
||||||
|
from codelist.settings import CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
|
||||||
|
|
||||||
|
|
||||||
|
def migrate_975_to_288(apps, schema_editor):
|
||||||
|
KonovaCodeList = apps.get_model('codelist', 'KonovaCodeList')
|
||||||
|
CompensationState = apps.get_model('compensation', 'CompensationState')
|
||||||
|
|
||||||
|
try:
|
||||||
|
list_288 = KonovaCodeList.objects.get(
|
||||||
|
id=CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
|
).codes.all()
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise AssertionError("KonovaCodeList 288 does not exist. Did you run 'update_codelist' before migrating?")
|
||||||
|
|
||||||
|
states_with_extra_code = CompensationState.objects.filter(
|
||||||
|
~Q(biotope_type_details=None)
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"... Found {states_with_extra_code.count()} biotope state entries")
|
||||||
|
for state in states_with_extra_code:
|
||||||
|
extra_codes_975 = state.biotope_type_details.filter(
|
||||||
|
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID]
|
||||||
|
)
|
||||||
|
count_extra_codes_975 = extra_codes_975.count()
|
||||||
|
if count_extra_codes_975 > 0:
|
||||||
|
print(f" --> Found {count_extra_codes_975} codes from list 975 in biotope entry {state.id}")
|
||||||
|
extra_codes_288 = []
|
||||||
|
for extra_code_975 in extra_codes_975:
|
||||||
|
atom_id = extra_code_975.atom_id
|
||||||
|
codes_from_288 = list_288.filter(
|
||||||
|
atom_id=atom_id,
|
||||||
|
)
|
||||||
|
extra_codes_288 += codes_from_288
|
||||||
|
|
||||||
|
state.biotope_type_details.set(extra_codes_288)
|
||||||
|
print(" --> Migrated to list 288 for all biotope entries")
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('codelist', '0001_initial'),
|
||||||
|
('compensation', '0003_auto_20220202_0846'),
|
||||||
|
]
|
||||||
|
|
||||||
|
# If migration of codelist is not necessary, this variable can shut down the logic whilst not disturbing the
|
||||||
|
# migration history
|
||||||
|
EXECUTE_CODELIST_MIGRATION = True
|
||||||
|
|
||||||
|
operations = []
|
||||||
|
|
||||||
|
if EXECUTE_CODELIST_MIGRATION:
|
||||||
|
operations.append(migrations.RunPython(migrate_975_to_288))
|
||||||
|
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.0.8 on 2024-08-26 16:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('codelist', '0002_migrate_975_to_288'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='konovacode',
|
||||||
|
name='long_name',
|
||||||
|
field=models.CharField(blank=True, default="", max_length=1000),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='konovacode',
|
||||||
|
name='short_name',
|
||||||
|
field=models.CharField(blank=True, default="", help_text='Short version of long name', max_length=500),
|
||||||
|
preserve_default=False,
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -25,13 +25,11 @@ class KonovaCode(models.Model):
|
|||||||
)
|
)
|
||||||
short_name = models.CharField(
|
short_name = models.CharField(
|
||||||
max_length=500,
|
max_length=500,
|
||||||
null=True,
|
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="Short version of long name",
|
help_text="Short version of long name",
|
||||||
)
|
)
|
||||||
long_name = models.CharField(
|
long_name = models.CharField(
|
||||||
max_length=1000,
|
max_length=1000,
|
||||||
null=True,
|
|
||||||
blank=True,
|
blank=True,
|
||||||
help_text="",
|
help_text="",
|
||||||
)
|
)
|
||||||
@@ -50,12 +48,28 @@ class KonovaCode(models.Model):
|
|||||||
|
|
||||||
def __str__(self, with_parent: bool = True):
|
def __str__(self, with_parent: bool = True):
|
||||||
ret_val = ""
|
ret_val = ""
|
||||||
if self.parent and self.parent.long_name and with_parent:
|
|
||||||
ret_val += self.parent.long_name + " > "
|
long_name = self.long_name
|
||||||
ret_val += self.long_name
|
short_name = self.short_name
|
||||||
if self.short_name and self.short_name != self.long_name:
|
|
||||||
# Only add short name, if we won't have stupid repition like 'thing a (thing a)' due to misused long-short names
|
both_names_exist = long_name is not None and short_name is not None
|
||||||
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
|
||||||
@@ -75,7 +89,8 @@ class KonovaCode(models.Model):
|
|||||||
return self
|
return self
|
||||||
|
|
||||||
children = KonovaCode.objects.filter(
|
children = KonovaCode.objects.filter(
|
||||||
parent=self
|
parent=self,
|
||||||
|
is_archived=False,
|
||||||
).order_by(
|
).order_by(
|
||||||
order_by
|
order_by
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ CODELIST_CONSERVATION_OFFICE_ID = 907 # CLNaturschutzbehörden
|
|||||||
CODELIST_REGISTRATION_OFFICE_ID = 1053 # CLZulassungsbehörden
|
CODELIST_REGISTRATION_OFFICE_ID = 1053 # CLZulassungsbehörden
|
||||||
CODELIST_BIOTOPES_ID = 654 # CL_Biotoptypen
|
CODELIST_BIOTOPES_ID = 654 # CL_Biotoptypen
|
||||||
CODELIST_AFTER_STATE_BIOTOPES_ID = 974 # CL-KSP_ZielBiotoptypen - USAGE HAS BEEN DROPPED IN 2022 IN FAVOR OF 654
|
CODELIST_AFTER_STATE_BIOTOPES_ID = 974 # CL-KSP_ZielBiotoptypen - USAGE HAS BEEN DROPPED IN 2022 IN FAVOR OF 654
|
||||||
CODELIST_BIOTOPES_EXTRA_CODES_ID = 975 # CLZusatzbezeichnung
|
CODELIST_BIOTOPES_EXTRA_CODES_ID = 975 # CLZusatzbezeichnung - Subset of 288. Migration usage 975->288 in 08/2024
|
||||||
|
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID = 288 # CLBiotoptypZusatzcode
|
||||||
CODELIST_LAW_ID = 1048 # CLVerfahrensrecht
|
CODELIST_LAW_ID = 1048 # CLVerfahrensrecht
|
||||||
CODELIST_PROCESS_TYPE_ID = 44382 # CLVerfahrenstyp
|
CODELIST_PROCESS_TYPE_ID = 44382 # CLVerfahrenstyp
|
||||||
|
|
||||||
|
|||||||
@@ -45,6 +45,14 @@ class AbstractCompensationAdmin(BaseObjectAdmin):
|
|||||||
states = "\n".join(states)
|
states = "\n".join(states)
|
||||||
return states
|
return states
|
||||||
|
|
||||||
|
def get_actions(self, request):
|
||||||
|
DELETE_ACTION_IDENTIFIER = "delete_selected"
|
||||||
|
actions = super().get_actions(request)
|
||||||
|
|
||||||
|
if DELETE_ACTION_IDENTIFIER in actions:
|
||||||
|
del actions[DELETE_ACTION_IDENTIFIER]
|
||||||
|
|
||||||
|
return actions
|
||||||
|
|
||||||
class CompensationAdmin(AbstractCompensationAdmin):
|
class CompensationAdmin(AbstractCompensationAdmin):
|
||||||
autocomplete_fields = [
|
autocomplete_fields = [
|
||||||
@@ -148,7 +156,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)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from compensation.models import EcoAccount
|
|||||||
from intervention.models import Handler, Responsibility, Legal
|
from intervention.models import Handler, Responsibility, Legal
|
||||||
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 ETS_GROUP
|
||||||
from konova.utils import validators
|
from konova.utils import validators
|
||||||
from user.models import User, UserActionLogEntry
|
from user.models import User, UserActionLogEntry
|
||||||
|
|
||||||
@@ -246,4 +247,13 @@ class RemoveEcoAccountModalForm(RemoveModalForm):
|
|||||||
"confirm",
|
"confirm",
|
||||||
_("The account can not be removed, since there are still deductions.")
|
_("The account can not be removed, since there are still deductions.")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If there are deductions but the performing user is not part of an ETS group, we assume this poor
|
||||||
|
# fella does not know what he/she does -> give a hint that they should contact someone in charge...
|
||||||
|
user_is_ets_user = self.user.in_group(ETS_GROUP)
|
||||||
|
if not user_is_ets_user:
|
||||||
|
self.add_error(
|
||||||
|
"confirm",
|
||||||
|
_("Please contact the responsible conservation office to find a solution!")
|
||||||
|
)
|
||||||
return super_valid and not has_deductions
|
return super_valid and not has_deductions
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ from django.shortcuts import render
|
|||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
from codelist.models import KonovaCode
|
from codelist.models import KonovaCode
|
||||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
|
from codelist.settings import CODELIST_BIOTOPES_ID, \
|
||||||
|
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
from intervention.inputs import CompensationStateTreeRadioSelect
|
from intervention.inputs import CompensationStateTreeRadioSelect
|
||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
from konova.forms.modals import RemoveModalForm, BaseModalForm
|
from konova.forms.modals import RemoveModalForm, BaseModalForm
|
||||||
@@ -43,7 +44,7 @@ class NewCompensationStateModalForm(BaseModalForm):
|
|||||||
queryset=KonovaCode.objects.filter(
|
queryset=KonovaCode.objects.filter(
|
||||||
is_archived=False,
|
is_archived=False,
|
||||||
is_leaf=True,
|
is_leaf=True,
|
||||||
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID],
|
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID],
|
||||||
),
|
),
|
||||||
widget=autocomplete.ModelSelect2Multiple(
|
widget=autocomplete.ModelSelect2Multiple(
|
||||||
url="codelist:biotope-extra-type-autocomplete",
|
url="codelist:biotope-extra-type-autocomplete",
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
# Generated by Django 5.0.8 on 2024-08-26 16:47
|
||||||
|
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('codelist', '0003_alter_konovacode_long_name_and_more'),
|
||||||
|
('compensation', '0015_alter_compensation_after_states_and_more'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AlterField(
|
||||||
|
model_name='compensationstate',
|
||||||
|
name='biotope_type_details',
|
||||||
|
field=models.ManyToManyField(blank=True, limit_choices_to={'code_lists__in': [288], 'is_archived': False, 'is_selectable': True}, related_name='+', to='codelist.konovacode'),
|
||||||
|
),
|
||||||
|
]
|
||||||
@@ -6,10 +6,10 @@ Created on: 16.11.21
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.db.models import Q
|
|
||||||
|
|
||||||
from codelist.models import KonovaCode
|
from codelist.models import KonovaCode
|
||||||
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
|
from codelist.settings import CODELIST_BIOTOPES_ID, \
|
||||||
|
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
|
||||||
from compensation.managers import CompensationStateManager
|
from compensation.managers import CompensationStateManager
|
||||||
from konova.models import UuidModel
|
from konova.models import UuidModel
|
||||||
|
|
||||||
@@ -34,7 +34,7 @@ class CompensationState(UuidModel):
|
|||||||
KonovaCode,
|
KonovaCode,
|
||||||
blank=True,
|
blank=True,
|
||||||
limit_choices_to={
|
limit_choices_to={
|
||||||
"code_lists__in": [CODELIST_BIOTOPES_EXTRA_CODES_ID],
|
"code_lists__in": [CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID],
|
||||||
"is_selectable": True,
|
"is_selectable": True,
|
||||||
"is_archived": False,
|
"is_archived": False,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-action' obj.id %}" title="{% trans 'Add new action' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-action' obj.id %}" title="{% trans 'Add new action' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'seedling' %}
|
{% fa5_icon 'seedling' %}
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@@ -52,7 +52,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for detail in action.action_type_details.all %}
|
{% for detail in action.action_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
|
<button data-form-url="{% url 'compensation:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{% fa5_icon 'file-alt' %}
|
{% fa5_icon 'file-alt' %}
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:resubmission-create' obj.id %}">
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:resubmission-create' obj.id %}">
|
||||||
{% fa5_icon 'bell' %}
|
{% fa5_icon 'bell' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'calendar-check' %}
|
{% fa5_icon 'calendar-check' %}
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
|
<button data-form-url="{% url 'compensation:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'file' %}
|
{% fa5_icon 'file' %}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@@ -59,7 +59,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
<button data-form-url="{% url 'compensation:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'layer-group' %}
|
{% fa5_icon 'layer-group' %}
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Surface' %}
|
{% trans 'Surface' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@@ -51,14 +51,14 @@
|
|||||||
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
<br>
|
<br>
|
||||||
{% for detail in state.biotope_type_details.all %}
|
{% for detail in state.biotope_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'layer-group' %}
|
{% fa5_icon 'layer-group' %}
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Surface' %}
|
{% trans 'Surface' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@@ -51,14 +51,14 @@
|
|||||||
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
<br>
|
<br>
|
||||||
{% for detail in state.biotope_type_details.all %}
|
{% for detail in state.biotope_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -123,7 +123,7 @@
|
|||||||
{% include 'user/includes/team_data_modal_button.html' %}
|
{% include 'user/includes/team_data_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<hr>
|
<hr>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
{% for user in obj.intervention.shared_users %}
|
{% for user in obj.intervention.shared_users %}
|
||||||
{% include 'user/includes/contact_modal_button.html' %}
|
{% include 'user/includes/contact_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-action' obj.id %}" title="{% trans 'Add new action' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-action' obj.id %}" title="{% trans 'Add new action' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'seedling' %}
|
{% fa5_icon 'seedling' %}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for detail in action.action_type_details.all %}
|
{% for detail in action.action_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -63,7 +63,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:acc:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
|
<button data-form-url="{% url 'compensation:acc:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{% fa5_icon 'file-alt' %}
|
{% fa5_icon 'file-alt' %}
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:acc:resubmission-create' obj.id %}">
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:acc:resubmission-create' obj.id %}">
|
||||||
{% fa5_icon 'bell' %}
|
{% fa5_icon 'bell' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'calendar-check' %}
|
{% fa5_icon 'calendar-check' %}
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:acc:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
|
<button data-form-url="{% url 'compensation:acc:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -53,7 +53,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
{% if deduction.intervention.recorded %}
|
{% if deduction.intervention.recorded %}
|
||||||
<em title="{% trans 'Recorded on' %} {{obj.recorded.timestamp}} {% trans 'by' %} {{obj.recorded.user}}" class='fas fa-bookmark registered-bookmark'></em>
|
<em title="{% trans 'Recorded on' %} {{deduction.intervention.recorded.timestamp}} {% trans 'by' %} {{deduction.intervention.recorded.user}}" class='fas fa-bookmark registered-bookmark'></em>
|
||||||
{% else %}
|
{% else %}
|
||||||
<em title="{% trans 'Not recorded yet' %}" class='far fa-bookmark'></em>
|
<em title="{% trans 'Not recorded yet' %}" class='far fa-bookmark'></em>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
|
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
|
||||||
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
|
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access or is_default_member and user in deduction.intervention.shared_users %}
|
{% if is_default_member and is_entry_shared or is_default_member and user in deduction.intervention.shared_users %}
|
||||||
<button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
|
<button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'file' %}
|
{% fa5_icon 'file' %}
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:acc:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
<button data-form-url="{% url 'compensation:acc:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'layer-group' %}
|
{% fa5_icon 'layer-group' %}
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Surface' %}
|
{% trans 'Surface' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@@ -51,14 +51,14 @@
|
|||||||
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
<br>
|
<br>
|
||||||
{% for detail in state.biotope_type_details.all %}
|
{% for detail in state.biotope_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'layer-group' %}
|
{% fa5_icon 'layer-group' %}
|
||||||
@@ -35,7 +35,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Surface' %}
|
{% trans 'Surface' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@@ -51,14 +51,14 @@
|
|||||||
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
<br>
|
<br>
|
||||||
{% for detail in state.biotope_type_details.all %}
|
{% for detail in state.biotope_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
{% include 'user/includes/team_data_modal_button.html' %}
|
{% include 'user/includes/team_data_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<hr>
|
<hr>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
{% for user in obj.users.all %}
|
{% for user in obj.users.all %}
|
||||||
{% include 'user/includes/contact_modal_button.html' %}
|
{% include 'user/includes/contact_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
post_data = {
|
post_data = {
|
||||||
"identifier": test_id,
|
"identifier": test_id,
|
||||||
"title": test_title,
|
"title": test_title,
|
||||||
"geom": geom_json,
|
"output": geom_json,
|
||||||
"intervention": self.intervention.id,
|
"intervention": self.intervention.id,
|
||||||
}
|
}
|
||||||
pre_creation_intervention_log_count = self.intervention.log.count()
|
pre_creation_intervention_log_count = self.intervention.log.count()
|
||||||
@@ -94,7 +94,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
post_data = {
|
post_data = {
|
||||||
"identifier": test_id,
|
"identifier": test_id,
|
||||||
"title": test_title,
|
"title": test_title,
|
||||||
"geom": geom_json,
|
"output": geom_json,
|
||||||
}
|
}
|
||||||
pre_creation_intervention_log_count = self.intervention.log.count()
|
pre_creation_intervention_log_count = self.intervention.log.count()
|
||||||
|
|
||||||
@@ -150,7 +150,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
"title": new_title,
|
"title": new_title,
|
||||||
"intervention": self.intervention.id, # just keep the intervention as it is
|
"intervention": self.intervention.id, # just keep the intervention as it is
|
||||||
"comment": new_comment,
|
"comment": new_comment,
|
||||||
"geom": geojson,
|
"output": geojson,
|
||||||
}
|
}
|
||||||
self.client_user.post(url, post_data)
|
self.client_user.post(url, post_data)
|
||||||
self.compensation.refresh_from_db()
|
self.compensation.refresh_from_db()
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
post_data = {
|
post_data = {
|
||||||
"identifier": test_id,
|
"identifier": test_id,
|
||||||
"title": test_title,
|
"title": test_title,
|
||||||
"geom": geom_json,
|
"output": geom_json,
|
||||||
"surface": test_deductable_surface,
|
"surface": test_deductable_surface,
|
||||||
"conservation_office": test_conservation_office.id
|
"conservation_office": test_conservation_office.id
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
new_title = self.create_dummy_string()
|
new_title = self.create_dummy_string()
|
||||||
new_identifier = self.create_dummy_string()
|
new_identifier = self.create_dummy_string()
|
||||||
new_comment = self.create_dummy_string()
|
new_comment = self.create_dummy_string()
|
||||||
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
|
new_geometry = self.create_dummy_geometry()
|
||||||
test_conservation_office = self.get_conservation_office_code()
|
test_conservation_office = self.get_conservation_office_code()
|
||||||
test_deductable_surface = self.eco_account.deductable_surface + 100
|
test_deductable_surface = self.eco_account.deductable_surface + 100
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
"identifier": new_identifier,
|
"identifier": new_identifier,
|
||||||
"title": new_title,
|
"title": new_title,
|
||||||
"comment": new_comment,
|
"comment": new_comment,
|
||||||
"geom": new_geometry.geojson,
|
"output": self.create_geojson(new_geometry),
|
||||||
"surface": test_deductable_surface,
|
"surface": test_deductable_surface,
|
||||||
"conservation_office": test_conservation_office.id
|
"conservation_office": test_conservation_office.id
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,30 +7,32 @@ Created on: 24.08.21
|
|||||||
"""
|
"""
|
||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
|
from compensation.views.compensation.detail import DetailCompensationView
|
||||||
from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \
|
from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \
|
||||||
GetCompensationDocumentView, RemoveCompensationDocumentView
|
GetCompensationDocumentView, RemoveCompensationDocumentView
|
||||||
|
from compensation.views.compensation.remove import RemoveCompensationView
|
||||||
from compensation.views.compensation.resubmission import CompensationResubmissionView
|
from compensation.views.compensation.resubmission import CompensationResubmissionView
|
||||||
from compensation.views.compensation.report import report_view
|
from compensation.views.compensation.report import CompensationPublicReportView
|
||||||
from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \
|
from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \
|
||||||
RemoveCompensationDeadlineView
|
RemoveCompensationDeadlineView
|
||||||
from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \
|
from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \
|
||||||
RemoveCompensationActionView
|
RemoveCompensationActionView
|
||||||
from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \
|
from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \
|
||||||
RemoveCompensationStateView
|
RemoveCompensationStateView
|
||||||
from compensation.views.compensation.compensation import index_view, new_view, new_id_view, detail_view, edit_view, \
|
from compensation.views.compensation.compensation import IndexCompensationView, CompensationIdentifierGeneratorView, \
|
||||||
remove_view
|
EditCompensationView, NewCompensationView
|
||||||
from compensation.views.compensation.log import CompensationLogView
|
from compensation.views.compensation.log import CompensationLogView
|
||||||
|
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
# Main compensation
|
# Main compensation
|
||||||
path("", index_view, name="index"),
|
path("", IndexCompensationView.as_view(), name="index"),
|
||||||
path('new/id', new_id_view, name='new-id'),
|
path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'),
|
||||||
path('new/<intervention_id>', new_view, name='new'),
|
path('new/<intervention_id>', NewCompensationView.as_view(), name='new'),
|
||||||
path('new', new_view, name='new'),
|
path('new', NewCompensationView.as_view(), name='new'),
|
||||||
path('<id>', detail_view, name='detail'),
|
path('<id>', DetailCompensationView.as_view(), name='detail'),
|
||||||
path('<id>/log', CompensationLogView.as_view(), name='log'),
|
path('<id>/log', CompensationLogView.as_view(), name='log'),
|
||||||
path('<id>/edit', edit_view, name='edit'),
|
path('<id>/edit', EditCompensationView.as_view(), name='edit'),
|
||||||
path('<id>/remove', remove_view, name='remove'),
|
path('<id>/remove', RemoveCompensationView.as_view(), name='remove'),
|
||||||
|
|
||||||
path('<id>/state/new', NewCompensationStateView.as_view(), name='new-state'),
|
path('<id>/state/new', NewCompensationStateView.as_view(), name='new-state'),
|
||||||
path('<id>/state/<state_id>/edit', EditCompensationStateView.as_view(), name='state-edit'),
|
path('<id>/state/<state_id>/edit', EditCompensationStateView.as_view(), name='state-edit'),
|
||||||
@@ -43,7 +45,7 @@ urlpatterns = [
|
|||||||
path('<id>/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"),
|
path('<id>/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"),
|
||||||
path('<id>/deadline/<deadline_id>/edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'),
|
path('<id>/deadline/<deadline_id>/edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'),
|
||||||
path('<id>/deadline/<deadline_id>/remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'),
|
path('<id>/deadline/<deadline_id>/remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'),
|
||||||
path('<id>/report', report_view, name='report'),
|
path('<id>/report', CompensationPublicReportView.as_view(), name='report'),
|
||||||
path('<id>/resub', CompensationResubmissionView.as_view(), name='resubmission-create'),
|
path('<id>/resub', CompensationResubmissionView.as_view(), name='resubmission-create'),
|
||||||
|
|
||||||
# Documents
|
# Documents
|
||||||
|
|||||||
@@ -8,11 +8,13 @@ Created on: 24.08.21
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from compensation.autocomplete.eco_account import EcoAccountAutocomplete
|
from compensation.autocomplete.eco_account import EcoAccountAutocomplete
|
||||||
from compensation.views.eco_account.eco_account import index_view, new_view, new_id_view, edit_view, remove_view, \
|
from compensation.views.eco_account.detail import DetailEcoAccountView
|
||||||
detail_view
|
from compensation.views.eco_account.eco_account import IndexEcoAccountView, EcoAccountIdentifierGeneratorView, \
|
||||||
|
NewEcoAccountView, EditEcoAccountView
|
||||||
from compensation.views.eco_account.log import EcoAccountLogView
|
from compensation.views.eco_account.log import EcoAccountLogView
|
||||||
from compensation.views.eco_account.record import EcoAccountRecordView
|
from compensation.views.eco_account.record import EcoAccountRecordView
|
||||||
from compensation.views.eco_account.report import report_view
|
from compensation.views.eco_account.remove import RemoveEcoAccountView
|
||||||
|
from compensation.views.eco_account.report import EcoAccountPublicReportView
|
||||||
from compensation.views.eco_account.resubmission import EcoAccountResubmissionView
|
from compensation.views.eco_account.resubmission import EcoAccountResubmissionView
|
||||||
from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \
|
from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \
|
||||||
RemoveEcoAccountStateView
|
RemoveEcoAccountStateView
|
||||||
@@ -28,15 +30,15 @@ from compensation.views.eco_account.deduction import NewEcoAccountDeductionView,
|
|||||||
|
|
||||||
app_name = "acc"
|
app_name = "acc"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", index_view, name="index"),
|
path("", IndexEcoAccountView.as_view(), name="index"),
|
||||||
path('new/', new_view, name='new'),
|
path('new/', NewEcoAccountView.as_view(), name='new'),
|
||||||
path('new/id', new_id_view, name='new-id'),
|
path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'),
|
||||||
path('<id>', detail_view, name='detail'),
|
path('<id>', DetailEcoAccountView.as_view(), name='detail'),
|
||||||
path('<id>/log', EcoAccountLogView.as_view(), name='log'),
|
path('<id>/log', EcoAccountLogView.as_view(), name='log'),
|
||||||
path('<id>/record', EcoAccountRecordView.as_view(), name='record'),
|
path('<id>/record', EcoAccountRecordView.as_view(), name='record'),
|
||||||
path('<id>/report', report_view, name='report'),
|
path('<id>/report', EcoAccountPublicReportView.as_view(), name='report'),
|
||||||
path('<id>/edit', edit_view, name='edit'),
|
path('<id>/edit', EditEcoAccountView.as_view(), name='edit'),
|
||||||
path('<id>/remove', remove_view, name='remove'),
|
path('<id>/remove', RemoveEcoAccountView.as_view(), name='remove'),
|
||||||
path('<id>/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'),
|
path('<id>/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'),
|
||||||
|
|
||||||
path('<id>/state/new', NewEcoAccountStateView.as_view(), name='new-state'),
|
path('<id>/state/new', NewEcoAccountStateView.as_view(), name='new-state'),
|
||||||
|
|||||||
@@ -6,91 +6,127 @@ Created on: 19.08.22
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.db.models import Sum
|
|
||||||
from django.http import HttpRequest, JsonResponse
|
|
||||||
from django.shortcuts import get_object_or_404, render, redirect
|
from django.shortcuts import get_object_or_404, render, redirect
|
||||||
from django.urls import reverse
|
from django.utils.decorators import method_decorator
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
from compensation.forms.compensation import EditCompensationForm, NewCompensationForm
|
from compensation.forms.compensation import EditCompensationForm, NewCompensationForm
|
||||||
from compensation.models import Compensation
|
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
|
||||||
uuid_required
|
|
||||||
from konova.forms import SimpleGeomForm
|
from konova.forms import SimpleGeomForm
|
||||||
from konova.forms.modals import RemoveModalForm
|
|
||||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
|
||||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||||
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
|
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, \
|
||||||
RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \
|
IDENTIFIER_REPLACED, COMPENSATION_ADDED_TEMPLATE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
|
||||||
COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
|
from konova.views.identifier import AbstractIdentifierGeneratorView
|
||||||
|
from konova.views.index import AbstractIndexView
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class IndexCompensationView(AbstractIndexView):
|
||||||
@any_group_check
|
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||||
def index_view(request: HttpRequest):
|
"""
|
||||||
"""
|
Renders the index view for compensation
|
||||||
Renders the index view for compensation
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request (HttpRequest): The incoming request
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A rendered view
|
A rendered view
|
||||||
"""
|
"""
|
||||||
template = "generic_index.html"
|
compensations = Compensation.objects.filter(
|
||||||
compensations = Compensation.objects.filter(
|
deleted=None, # only show those which are not deleted individually
|
||||||
deleted=None, # only show those which are not deleted individually
|
intervention__deleted=None, # and don't show the ones whose intervention has been deleted
|
||||||
intervention__deleted=None, # and don't show the ones whose intervention has been deleted
|
).order_by(
|
||||||
).order_by(
|
"-modified__timestamp"
|
||||||
"-modified__timestamp"
|
)
|
||||||
)
|
table = CompensationTable(
|
||||||
table = CompensationTable(
|
request=request,
|
||||||
request=request,
|
queryset=compensations
|
||||||
queryset=compensations
|
)
|
||||||
)
|
context = {
|
||||||
context = {
|
"table": table,
|
||||||
"table": table,
|
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
|
||||||
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
|
}
|
||||||
}
|
context = BaseContext(request, context).context
|
||||||
context = BaseContext(request, context).context
|
return render(request, self._TEMPLATE, context)
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class NewCompensationView(LoginRequiredMixin, View):
|
||||||
@default_group_required
|
_TEMPLATE = "compensation/form/view.html"
|
||||||
@shared_access_required(Intervention, "intervention_id")
|
|
||||||
def new_view(request: HttpRequest, intervention_id: str = None):
|
|
||||||
"""
|
|
||||||
Renders a view for a new compensation creation
|
|
||||||
|
|
||||||
Args:
|
@method_decorator(default_group_required)
|
||||||
request (HttpRequest): The incoming request
|
@method_decorator(shared_access_required(Intervention, "intervention_id"))
|
||||||
|
def get(self, request: HttpRequest, intervention_id: str = None, *args, **kwargs) -> HttpResponse:
|
||||||
|
"""
|
||||||
|
Renders a view for new compensation
|
||||||
|
|
||||||
Returns:
|
A compensation creation may be called directly from the parent-intervention object. If so - we may take
|
||||||
|
the intervention's id and directly link the compensation to it.
|
||||||
|
|
||||||
"""
|
Args:
|
||||||
template = "compensation/form/view.html"
|
request (HttpRequest): The incoming request
|
||||||
if intervention_id is not None:
|
intervention_id (str): The intervention identifier
|
||||||
try:
|
|
||||||
intervention = Intervention.objects.get(id=intervention_id)
|
Returns:
|
||||||
except ObjectDoesNotExist:
|
|
||||||
messages.error(request, PARAMS_INVALID)
|
"""
|
||||||
return redirect("home")
|
if intervention_id:
|
||||||
if intervention.is_recorded:
|
# If the parent-intervention is recorded, we are not allowed to change anything on it's data.
|
||||||
messages.info(
|
# Not even adding new child elements like compensations!
|
||||||
request,
|
intervention = get_object_or_404(Intervention, id=intervention_id)
|
||||||
RECORDED_BLOCKS_EDIT
|
recording_state_blocks_actions = intervention.is_recorded
|
||||||
)
|
if recording_state_blocks_actions:
|
||||||
return redirect("intervention:detail", id=intervention_id)
|
messages.info(
|
||||||
|
request,
|
||||||
|
RECORDED_BLOCKS_EDIT
|
||||||
|
)
|
||||||
|
return redirect("intervention:detail", id=intervention_id)
|
||||||
|
|
||||||
|
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
|
||||||
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"form": data_form,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
TAB_TITLE_IDENTIFIER: _("New compensation"),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
|
@method_decorator(default_group_required)
|
||||||
|
@method_decorator(shared_access_required(Intervention, "intervention_id"))
|
||||||
|
def post(self, request: HttpRequest, intervention_id: str = None, *args, **kwargs) -> HttpResponse:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Renders a view for a new compensation creation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
if intervention_id:
|
||||||
|
# If the parent-intervention is recorded, we are not allowed to change anything on it's data.
|
||||||
|
# Not even adding new child elements like compensations!
|
||||||
|
intervention = get_object_or_404(Intervention, id=intervention_id)
|
||||||
|
recording_state_blocks_actions = intervention.is_recorded
|
||||||
|
if recording_state_blocks_actions:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
RECORDED_BLOCKS_EDIT
|
||||||
|
)
|
||||||
|
return redirect("intervention:detail", id=intervention_id)
|
||||||
|
|
||||||
|
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
|
||||||
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
||||||
|
|
||||||
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
|
|
||||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
|
||||||
if request.method == "POST":
|
|
||||||
if data_form.is_valid() and geom_form.is_valid():
|
if data_form.is_valid() and geom_form.is_valid():
|
||||||
generated_identifier = data_form.cleaned_data.get("identifier", None)
|
generated_identifier = data_form.cleaned_data.get("identifier", None)
|
||||||
comp = data_form.save(request.user, geom_form)
|
comp = data_form.save(request.user, geom_form)
|
||||||
@@ -103,199 +139,127 @@ def new_view(request: HttpRequest, intervention_id: str = None):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
|
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
|
||||||
if geom_form.geometry_simplified:
|
if geom_form.has_geometry_simplified():
|
||||||
messages.info(
|
messages.info(
|
||||||
request,
|
request,
|
||||||
GEOMETRY_SIMPLIFIED
|
GEOMETRY_SIMPLIFIED
|
||||||
)
|
)
|
||||||
|
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||||
|
if num_ignored_geometries > 0:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||||
|
)
|
||||||
return redirect("compensation:detail", id=comp.id)
|
return redirect("compensation:detail", id=comp.id)
|
||||||
else:
|
else:
|
||||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
messages.error(request, FORM_INVALID, extra_tags="danger", )
|
||||||
else:
|
|
||||||
# For clarification: nothing in this case
|
|
||||||
pass
|
|
||||||
context = {
|
|
||||||
"form": data_form,
|
|
||||||
"geom_form": geom_form,
|
|
||||||
TAB_TITLE_IDENTIFIER: _("New compensation"),
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
context = {
|
||||||
@login_required
|
"form": data_form,
|
||||||
@default_group_required
|
"geom_form": geom_form,
|
||||||
def new_id_view(request: HttpRequest):
|
TAB_TITLE_IDENTIFIER: _("New compensation"),
|
||||||
""" JSON endpoint
|
|
||||||
|
|
||||||
Provides fetching of free identifiers for e.g. AJAX calls
|
|
||||||
|
|
||||||
"""
|
|
||||||
tmp = Compensation()
|
|
||||||
identifier = tmp.generate_new_identifier()
|
|
||||||
while Compensation.objects.filter(identifier=identifier).exists():
|
|
||||||
identifier = tmp.generate_new_identifier()
|
|
||||||
return JsonResponse(
|
|
||||||
data={
|
|
||||||
"gen_data": identifier
|
|
||||||
}
|
}
|
||||||
)
|
context = BaseContext(request, context).context
|
||||||
|
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class CompensationIdentifierGeneratorView(AbstractIdentifierGeneratorView):
|
||||||
@default_group_required
|
_MODEL = Compensation
|
||||||
@shared_access_required(Compensation, "id")
|
|
||||||
def edit_view(request: HttpRequest, id: str):
|
|
||||||
"""
|
|
||||||
Renders a view for editing compensations
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (HttpRequest): The incoming request
|
|
||||||
|
|
||||||
Returns:
|
class EditCompensationView(LoginRequiredMixin, View):
|
||||||
|
_TEMPLATE = "compensation/form/view.html"
|
||||||
|
|
||||||
"""
|
@method_decorator(default_group_required)
|
||||||
template = "compensation/form/view.html"
|
@method_decorator(shared_access_required(Compensation, "id"))
|
||||||
# Get object from db
|
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
comp = get_object_or_404(Compensation, id=id)
|
|
||||||
if comp.is_recorded:
|
|
||||||
messages.info(
|
|
||||||
request,
|
|
||||||
RECORDED_BLOCKS_EDIT
|
|
||||||
)
|
|
||||||
return redirect("compensation:detail", id=id)
|
|
||||||
|
|
||||||
# Create forms, initialize with values from db/from POST request
|
"""
|
||||||
data_form = EditCompensationForm(request.POST or None, instance=comp)
|
Renders a view for editing compensations
|
||||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
|
|
||||||
if request.method == "POST":
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get object from db
|
||||||
|
comp = get_object_or_404(Compensation, id=id)
|
||||||
|
if comp.is_recorded:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
RECORDED_BLOCKS_EDIT
|
||||||
|
)
|
||||||
|
return redirect("compensation:detail", id=id)
|
||||||
|
|
||||||
|
# Create forms, initialize with values from db/from POST request
|
||||||
|
data_form = EditCompensationForm(request.POST or None, instance=comp)
|
||||||
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"form": data_form,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
|
@method_decorator(default_group_required)
|
||||||
|
@method_decorator(shared_access_required(Compensation, "id"))
|
||||||
|
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Renders a view for editing compensations
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get object from db
|
||||||
|
comp = get_object_or_404(Compensation, id=id)
|
||||||
|
if comp.is_recorded:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
RECORDED_BLOCKS_EDIT
|
||||||
|
)
|
||||||
|
return redirect("compensation:detail", id=id)
|
||||||
|
|
||||||
|
# Create forms, initialize with values from db/from POST request
|
||||||
|
data_form = EditCompensationForm(request.POST or None, instance=comp)
|
||||||
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
|
||||||
if data_form.is_valid() and geom_form.is_valid():
|
if data_form.is_valid() and geom_form.is_valid():
|
||||||
# Preserve state of intervention checked to determine whether the user must be informed or not
|
# Preserve state of intervention checked to determine whether the user must be informed or not
|
||||||
# about a change of the check state
|
# about a change of the check state
|
||||||
intervention_is_checked = comp.intervention.checked is not None
|
intervention_is_checked = comp.intervention.checked is not None
|
||||||
|
|
||||||
# The data form takes the geom form for processing, as well as the performing user
|
# The data form takes the geom form for processing, as well as the performing user
|
||||||
comp = data_form.save(request.user, geom_form)
|
comp = data_form.save(request.user, geom_form)
|
||||||
if intervention_is_checked:
|
if intervention_is_checked:
|
||||||
messages.info(request, CHECK_STATE_RESET)
|
messages.info(request, CHECK_STATE_RESET)
|
||||||
messages.success(request, _("Compensation {} edited").format(comp.identifier))
|
messages.success(request, _("Compensation {} edited").format(comp.identifier))
|
||||||
if geom_form.geometry_simplified:
|
if geom_form.has_geometry_simplified():
|
||||||
messages.info(
|
messages.info(
|
||||||
request,
|
request,
|
||||||
GEOMETRY_SIMPLIFIED
|
GEOMETRY_SIMPLIFIED
|
||||||
)
|
)
|
||||||
|
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||||
|
if num_ignored_geometries > 0:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||||
|
)
|
||||||
return redirect("compensation:detail", id=comp.id)
|
return redirect("compensation:detail", id=comp.id)
|
||||||
else:
|
else:
|
||||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
messages.error(request, FORM_INVALID, extra_tags="danger", )
|
||||||
else:
|
|
||||||
# For clarification: nothing in this case
|
|
||||||
pass
|
|
||||||
context = {
|
|
||||||
"form": data_form,
|
|
||||||
"geom_form": geom_form,
|
|
||||||
TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier),
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"form": data_form,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
|
||||||
@login_required
|
return render(request, self._TEMPLATE, context)
|
||||||
@any_group_check
|
|
||||||
@uuid_required
|
|
||||||
def detail_view(request: HttpRequest, id: str):
|
|
||||||
""" Renders a detail view for a compensation
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (HttpRequest): The incoming request
|
|
||||||
id (str): The compensation's id
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
template = "compensation/detail/compensation/view.html"
|
|
||||||
comp = get_object_or_404(
|
|
||||||
Compensation.objects.select_related(
|
|
||||||
"modified",
|
|
||||||
"created",
|
|
||||||
"geometry"
|
|
||||||
),
|
|
||||||
id=id,
|
|
||||||
deleted=None,
|
|
||||||
intervention__deleted=None,
|
|
||||||
)
|
|
||||||
geom_form = SimpleGeomForm(instance=comp)
|
|
||||||
parcels = comp.get_underlying_parcels()
|
|
||||||
_user = request.user
|
|
||||||
is_data_shared = comp.intervention.is_shared_with(_user)
|
|
||||||
|
|
||||||
# Order states according to surface
|
|
||||||
before_states = comp.before_states.all().prefetch_related("biotope_type").order_by("-surface")
|
|
||||||
after_states = comp.after_states.all().prefetch_related("biotope_type").order_by("-surface")
|
|
||||||
actions = comp.actions.all().prefetch_related("action_type")
|
|
||||||
|
|
||||||
# Precalculate logical errors between before- and after-states
|
|
||||||
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
|
|
||||||
sum_before_states = comp.get_surface_before_states()
|
|
||||||
sum_after_states = comp.get_surface_after_states()
|
|
||||||
diff_states = abs(sum_before_states - sum_after_states)
|
|
||||||
|
|
||||||
request = comp.set_status_messages(request)
|
|
||||||
|
|
||||||
last_checked = comp.intervention.get_last_checked_action()
|
|
||||||
last_checked_tooltip = ""
|
|
||||||
if last_checked:
|
|
||||||
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
|
|
||||||
|
|
||||||
requesting_user_is_only_shared_user = comp.is_only_shared_with(_user)
|
|
||||||
if requesting_user_is_only_shared_user:
|
|
||||||
messages.info(
|
|
||||||
request,
|
|
||||||
DO_NOT_FORGET_TO_SHARE
|
|
||||||
)
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"obj": comp,
|
|
||||||
"last_checked": last_checked,
|
|
||||||
"last_checked_tooltip": last_checked_tooltip,
|
|
||||||
"geom_form": geom_form,
|
|
||||||
"parcels": parcels,
|
|
||||||
"has_access": is_data_shared,
|
|
||||||
"actions": actions,
|
|
||||||
"before_states": before_states,
|
|
||||||
"after_states": after_states,
|
|
||||||
"sum_before_states": sum_before_states,
|
|
||||||
"sum_after_states": sum_after_states,
|
|
||||||
"diff_states": diff_states,
|
|
||||||
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
|
||||||
"is_zb_member": _user.in_group(ZB_GROUP),
|
|
||||||
"is_ets_member": _user.in_group(ETS_GROUP),
|
|
||||||
"LANIS_LINK": comp.get_LANIS_link(),
|
|
||||||
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}",
|
|
||||||
"has_finished_deadlines": comp.get_finished_deadlines().exists(),
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required_modal
|
|
||||||
@login_required
|
|
||||||
@default_group_required
|
|
||||||
@shared_access_required(Compensation, "id")
|
|
||||||
def remove_view(request: HttpRequest, id: str):
|
|
||||||
""" Renders a modal view for removing the compensation
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (HttpRequest): The incoming request
|
|
||||||
id (str): The compensation's id
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
comp = get_object_or_404(Compensation, id=id)
|
|
||||||
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
|
|
||||||
return form.process_request(
|
|
||||||
request=request,
|
|
||||||
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
|
|
||||||
redirect_url=reverse("compensation:index"),
|
|
||||||
)
|
|
||||||
|
|
||||||
97
compensation/views/compensation/detail.py
Normal file
97
compensation/views/compensation/detail.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Created on: 14.12.25
|
||||||
|
|
||||||
|
"""
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.shortcuts import render, get_object_or_404
|
||||||
|
|
||||||
|
from compensation.models import Compensation
|
||||||
|
from konova.contexts import BaseContext
|
||||||
|
from konova.forms import SimpleGeomForm
|
||||||
|
from konova.settings import ETS_GROUP, ZB_GROUP, DEFAULT_GROUP
|
||||||
|
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||||
|
from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE, DATA_CHECKED_PREVIOUSLY_TEMPLATE
|
||||||
|
from konova.views.detail import AbstractDetailView
|
||||||
|
|
||||||
|
|
||||||
|
class DetailCompensationView(AbstractDetailView):
|
||||||
|
_TEMPLATE = "compensation/detail/compensation/view.html"
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
|
||||||
|
""" Renders a detail view for a compensation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): The compensation's id
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
comp = get_object_or_404(
|
||||||
|
Compensation.objects.select_related(
|
||||||
|
"modified",
|
||||||
|
"created",
|
||||||
|
"geometry"
|
||||||
|
),
|
||||||
|
id=id,
|
||||||
|
deleted=None,
|
||||||
|
intervention__deleted=None,
|
||||||
|
)
|
||||||
|
geom_form = SimpleGeomForm(instance=comp)
|
||||||
|
parcels = comp.get_underlying_parcels()
|
||||||
|
_user = request.user
|
||||||
|
is_data_shared = comp.intervention.is_shared_with(_user)
|
||||||
|
|
||||||
|
# Order states according to surface
|
||||||
|
before_states = comp.before_states.all().prefetch_related("biotope_type").order_by("-surface")
|
||||||
|
after_states = comp.after_states.all().prefetch_related("biotope_type").order_by("-surface")
|
||||||
|
actions = comp.actions.all().prefetch_related("action_type")
|
||||||
|
|
||||||
|
# Precalculate logical errors between before- and after-states
|
||||||
|
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
|
||||||
|
sum_before_states = comp.get_surface_before_states()
|
||||||
|
sum_after_states = comp.get_surface_after_states()
|
||||||
|
diff_states = abs(sum_before_states - sum_after_states)
|
||||||
|
|
||||||
|
request = comp.set_status_messages(request)
|
||||||
|
|
||||||
|
last_checked = comp.intervention.get_last_checked_action()
|
||||||
|
last_checked_tooltip = ""
|
||||||
|
if last_checked:
|
||||||
|
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
|
||||||
|
last_checked.get_timestamp_str_formatted(),
|
||||||
|
last_checked.user
|
||||||
|
)
|
||||||
|
|
||||||
|
requesting_user_is_only_shared_user = comp.is_only_shared_with(_user)
|
||||||
|
if requesting_user_is_only_shared_user:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
DO_NOT_FORGET_TO_SHARE
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"obj": comp,
|
||||||
|
"last_checked": last_checked,
|
||||||
|
"last_checked_tooltip": last_checked_tooltip,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
"parcels": parcels,
|
||||||
|
"is_entry_shared": is_data_shared,
|
||||||
|
"actions": actions,
|
||||||
|
"before_states": before_states,
|
||||||
|
"after_states": after_states,
|
||||||
|
"sum_before_states": sum_before_states,
|
||||||
|
"sum_after_states": sum_after_states,
|
||||||
|
"diff_states": diff_states,
|
||||||
|
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
||||||
|
"is_zb_member": _user.in_group(ZB_GROUP),
|
||||||
|
"is_ets_member": _user.in_group(ETS_GROUP),
|
||||||
|
"LANIS_LINK": comp.get_LANIS_link(),
|
||||||
|
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}",
|
||||||
|
"has_finished_deadlines": comp.get_finished_deadlines().exists(),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
20
compensation/views/compensation/remove.py
Normal file
20
compensation/views/compensation/remove.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Created on: 14.12.25
|
||||||
|
|
||||||
|
"""
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from compensation.models import Compensation
|
||||||
|
from konova.decorators import shared_access_required
|
||||||
|
from konova.views.remove import AbstractRemoveView
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveCompensationView(AbstractRemoveView):
|
||||||
|
_MODEL = Compensation
|
||||||
|
_REDIRECT_URL = "compensation:index"
|
||||||
|
|
||||||
|
@method_decorator(shared_access_required(Compensation, "id"))
|
||||||
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
@@ -5,7 +5,7 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
|||||||
Created on: 19.08.22
|
Created on: 19.08.22
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import 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 _
|
||||||
@@ -14,67 +14,72 @@ from compensation.models import Compensation
|
|||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
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.qrcode import QrCode
|
||||||
|
from konova.views.report import AbstractPublicReportView
|
||||||
|
|
||||||
|
|
||||||
def report_view(request: HttpRequest, id: str):
|
class CompensationPublicReportView(AbstractPublicReportView):
|
||||||
""" Renders the public report view
|
_TEMPLATE = "compensation/report/compensation/report.html"
|
||||||
|
|
||||||
Args:
|
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
request (HttpRequest): The incoming request
|
""" Renders the public report view
|
||||||
id (str): The id of the intervention
|
|
||||||
|
|
||||||
Returns:
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): The id of the intervention
|
||||||
|
|
||||||
"""
|
Returns:
|
||||||
# Reuse the compensation report template since compensations are structurally identical
|
|
||||||
template = "compensation/report/compensation/report.html"
|
"""
|
||||||
comp = get_object_or_404(Compensation, id=id)
|
comp = get_object_or_404(Compensation, id=id)
|
||||||
|
tab_title = _("Report {}").format(comp.identifier)
|
||||||
|
# If intervention is not recorded (yet or currently) we need to render another template without any data
|
||||||
|
if not comp.is_ready_for_publish():
|
||||||
|
template = "report/unavailable.html"
|
||||||
|
context = {
|
||||||
|
TAB_TITLE_IDENTIFIER: tab_title,
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, template, context)
|
||||||
|
|
||||||
|
# Prepare data for map viewer
|
||||||
|
geom_form = SimpleGeomForm(
|
||||||
|
instance=comp
|
||||||
|
)
|
||||||
|
parcels = comp.get_underlying_parcels()
|
||||||
|
|
||||||
|
qrcode = QrCode(
|
||||||
|
content=request.build_absolute_uri(reverse("compensation:report", args=(id,))),
|
||||||
|
size=10
|
||||||
|
)
|
||||||
|
qrcode_lanis = QrCode(
|
||||||
|
content=comp.get_LANIS_link(),
|
||||||
|
size=7
|
||||||
|
)
|
||||||
|
|
||||||
|
# Order states by surface
|
||||||
|
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||||
|
after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||||
|
actions = comp.actions.all().prefetch_related("action_type")
|
||||||
|
|
||||||
tab_title = _("Report {}").format(comp.identifier)
|
|
||||||
# If intervention is not recorded (yet or currently) we need to render another template without any data
|
|
||||||
if not comp.is_ready_for_publish():
|
|
||||||
template = "report/unavailable.html"
|
|
||||||
context = {
|
context = {
|
||||||
|
"obj": comp,
|
||||||
|
"qrcode": {
|
||||||
|
"img": qrcode.get_img(),
|
||||||
|
"url": qrcode.get_content(),
|
||||||
|
},
|
||||||
|
"qrcode_lanis": {
|
||||||
|
"img": qrcode_lanis.get_img(),
|
||||||
|
"url": qrcode_lanis.get_content(),
|
||||||
|
},
|
||||||
|
"is_entry_shared": False, # disables action buttons during rendering
|
||||||
|
"before_states": before_states,
|
||||||
|
"after_states": after_states,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
"parcels": parcels,
|
||||||
|
"actions": actions,
|
||||||
|
"tables_scrollable": False,
|
||||||
TAB_TITLE_IDENTIFIER: tab_title,
|
TAB_TITLE_IDENTIFIER: tab_title,
|
||||||
}
|
}
|
||||||
context = BaseContext(request, context).context
|
context = BaseContext(request, context).context
|
||||||
return render(request, template, context)
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
# Prepare data for map viewer
|
|
||||||
geom_form = SimpleGeomForm(
|
|
||||||
instance=comp
|
|
||||||
)
|
|
||||||
parcels = comp.get_underlying_parcels()
|
|
||||||
|
|
||||||
qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,)))
|
|
||||||
qrcode_img = generate_qr_code(qrcode_url, 10)
|
|
||||||
qrcode_lanis_url = comp.get_LANIS_link()
|
|
||||||
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
|
|
||||||
|
|
||||||
# Order states by surface
|
|
||||||
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
|
|
||||||
after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type")
|
|
||||||
actions = comp.actions.all().prefetch_related("action_type")
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"obj": comp,
|
|
||||||
"qrcode": {
|
|
||||||
"img": qrcode_img,
|
|
||||||
"url": qrcode_url,
|
|
||||||
},
|
|
||||||
"qrcode_lanis": {
|
|
||||||
"img": qrcode_img_lanis,
|
|
||||||
"url": qrcode_lanis_url,
|
|
||||||
},
|
|
||||||
"has_access": False, # disables action buttons during rendering
|
|
||||||
"before_states": before_states,
|
|
||||||
"after_states": after_states,
|
|
||||||
"geom_form": geom_form,
|
|
||||||
"parcels": parcels,
|
|
||||||
"actions": actions,
|
|
||||||
"tables_scrollable": False,
|
|
||||||
TAB_TITLE_IDENTIFIER: tab_title,
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|||||||
97
compensation/views/eco_account/detail.py
Normal file
97
compensation/views/eco_account/detail.py
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Created on: 14.12.25
|
||||||
|
|
||||||
|
"""
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.shortcuts import render, get_object_or_404
|
||||||
|
|
||||||
|
from compensation.models import EcoAccount
|
||||||
|
from konova.contexts import BaseContext
|
||||||
|
from konova.forms import SimpleGeomForm
|
||||||
|
from konova.settings import ETS_GROUP, ZB_GROUP, DEFAULT_GROUP
|
||||||
|
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||||
|
from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE
|
||||||
|
from konova.views.detail import AbstractDetailView
|
||||||
|
|
||||||
|
|
||||||
|
class DetailEcoAccountView(AbstractDetailView):
|
||||||
|
_TEMPLATE = "compensation/detail/eco_account/view.html"
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
""" Renders a detail view for a compensation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): The compensation's id
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
acc = get_object_or_404(
|
||||||
|
EcoAccount.objects.prefetch_related(
|
||||||
|
"deadlines",
|
||||||
|
).select_related(
|
||||||
|
'geometry',
|
||||||
|
'responsible',
|
||||||
|
),
|
||||||
|
id=id,
|
||||||
|
deleted=None,
|
||||||
|
)
|
||||||
|
geom_form = SimpleGeomForm(instance=acc)
|
||||||
|
parcels = acc.get_underlying_parcels()
|
||||||
|
_user = request.user
|
||||||
|
is_data_shared = acc.is_shared_with(_user)
|
||||||
|
|
||||||
|
# Order states according to surface
|
||||||
|
before_states = acc.before_states.order_by("-surface")
|
||||||
|
after_states = acc.after_states.order_by("-surface")
|
||||||
|
|
||||||
|
# Precalculate logical errors between before- and after-states
|
||||||
|
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
|
||||||
|
sum_before_states = acc.get_surface_before_states()
|
||||||
|
sum_after_states = acc.get_surface_after_states()
|
||||||
|
diff_states = abs(sum_before_states - sum_after_states)
|
||||||
|
# Calculate rest of available surface for deductions
|
||||||
|
available_total = acc.deductable_rest
|
||||||
|
available_relative = acc.get_deductable_rest_relative()
|
||||||
|
|
||||||
|
# Prefetch related data to decrease the amount of db connections
|
||||||
|
deductions = acc.deductions.filter(
|
||||||
|
intervention__deleted=None,
|
||||||
|
)
|
||||||
|
actions = acc.actions.all()
|
||||||
|
|
||||||
|
request = acc.set_status_messages(request)
|
||||||
|
|
||||||
|
requesting_user_is_only_shared_user = acc.is_only_shared_with(_user)
|
||||||
|
if requesting_user_is_only_shared_user:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
DO_NOT_FORGET_TO_SHARE
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"obj": acc,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
"parcels": parcels,
|
||||||
|
"is_entry_shared": is_data_shared,
|
||||||
|
"before_states": before_states,
|
||||||
|
"after_states": after_states,
|
||||||
|
"sum_before_states": sum_before_states,
|
||||||
|
"sum_after_states": sum_after_states,
|
||||||
|
"diff_states": diff_states,
|
||||||
|
"available": available_relative,
|
||||||
|
"available_total": available_total,
|
||||||
|
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
||||||
|
"is_zb_member": _user.in_group(ZB_GROUP),
|
||||||
|
"is_ets_member": _user.in_group(ETS_GROUP),
|
||||||
|
"LANIS_LINK": acc.get_LANIS_link(),
|
||||||
|
"deductions": deductions,
|
||||||
|
"actions": actions,
|
||||||
|
TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}",
|
||||||
|
"has_finished_deadlines": acc.get_finished_deadlines().exists(),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
@@ -6,72 +6,94 @@ Created on: 19.08.22
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.db.models import Sum
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.http import HttpRequest, JsonResponse
|
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.utils.decorators import method_decorator
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm, RemoveEcoAccountModalForm
|
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
|
||||||
uuid_required
|
|
||||||
from konova.forms import SimpleGeomForm
|
from konova.forms import SimpleGeomForm
|
||||||
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
|
||||||
from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
|
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, FORM_INVALID, \
|
||||||
IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
|
IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
|
||||||
|
from konova.views.identifier import AbstractIdentifierGeneratorView
|
||||||
|
from konova.views.index import AbstractIndexView
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class IndexEcoAccountView(AbstractIndexView):
|
||||||
@any_group_check
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
def index_view(request: HttpRequest):
|
"""
|
||||||
"""
|
Renders the index view for eco accounts
|
||||||
Renders the index view for eco accounts
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request (HttpRequest): The incoming request
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A rendered view
|
A rendered view
|
||||||
"""
|
"""
|
||||||
template = "generic_index.html"
|
eco_accounts = EcoAccount.objects.filter(
|
||||||
eco_accounts = EcoAccount.objects.filter(
|
deleted=None,
|
||||||
deleted=None,
|
).order_by(
|
||||||
).order_by(
|
"-modified__timestamp"
|
||||||
"-modified__timestamp"
|
)
|
||||||
)
|
table = EcoAccountTable(
|
||||||
table = EcoAccountTable(
|
request=request,
|
||||||
request=request,
|
queryset=eco_accounts
|
||||||
queryset=eco_accounts
|
)
|
||||||
)
|
context = {
|
||||||
context = {
|
"table": table,
|
||||||
"table": table,
|
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
|
||||||
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
|
}
|
||||||
}
|
context = BaseContext(request, context).context
|
||||||
context = BaseContext(request, context).context
|
return render(request, self._TEMPLATE, context)
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class NewEcoAccountView(LoginRequiredMixin, View):
|
||||||
@default_group_required
|
_TEMPLATE = "compensation/form/view.html"
|
||||||
def new_view(request: HttpRequest):
|
|
||||||
"""
|
|
||||||
Renders a view for a new eco account creation
|
|
||||||
|
|
||||||
Args:
|
@method_decorator(default_group_required)
|
||||||
request (HttpRequest): The incoming request
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
"""
|
||||||
|
Renders a view for a new eco account creation
|
||||||
|
|
||||||
Returns:
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
"""
|
Returns:
|
||||||
template = "compensation/form/view.html"
|
|
||||||
data_form = NewEcoAccountForm(request.POST or None)
|
"""
|
||||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
data_form = NewEcoAccountForm(request.POST or None)
|
||||||
if request.method == "POST":
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"form": data_form,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
TAB_TITLE_IDENTIFIER: _("New Eco-Account"),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
|
@method_decorator(default_group_required)
|
||||||
|
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Renders a view for a new eco account creation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
data_form = NewEcoAccountForm(request.POST or None)
|
||||||
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
||||||
if data_form.is_valid() and geom_form.is_valid():
|
if data_form.is_valid() and geom_form.is_valid():
|
||||||
generated_identifier = data_form.cleaned_data.get("identifier", None)
|
generated_identifier = data_form.cleaned_data.get("identifier", None)
|
||||||
acc = data_form.save(request.user, geom_form)
|
acc = data_form.save(request.user, geom_form)
|
||||||
@@ -84,208 +106,123 @@ def new_view(request: HttpRequest):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
messages.success(request, _("Eco-Account {} added").format(acc.identifier))
|
messages.success(request, _("Eco-Account {} added").format(acc.identifier))
|
||||||
if geom_form.geometry_simplified:
|
if geom_form.has_geometry_simplified():
|
||||||
messages.info(
|
messages.info(
|
||||||
request,
|
request,
|
||||||
GEOMETRY_SIMPLIFIED
|
GEOMETRY_SIMPLIFIED
|
||||||
)
|
)
|
||||||
|
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||||
|
if num_ignored_geometries > 0:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||||
|
)
|
||||||
return redirect("compensation:acc:detail", id=acc.id)
|
return redirect("compensation:acc:detail", id=acc.id)
|
||||||
else:
|
else:
|
||||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
messages.error(request, FORM_INVALID, extra_tags="danger", )
|
||||||
else:
|
|
||||||
# For clarification: nothing in this case
|
|
||||||
pass
|
|
||||||
context = {
|
|
||||||
"form": data_form,
|
|
||||||
"geom_form": geom_form,
|
|
||||||
TAB_TITLE_IDENTIFIER: _("New Eco-Account"),
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
context = {
|
||||||
@login_required
|
"form": data_form,
|
||||||
@default_group_required
|
"geom_form": geom_form,
|
||||||
def new_id_view(request: HttpRequest):
|
TAB_TITLE_IDENTIFIER: _("New Eco-Account"),
|
||||||
""" JSON endpoint
|
|
||||||
|
|
||||||
Provides fetching of free identifiers for e.g. AJAX calls
|
|
||||||
|
|
||||||
"""
|
|
||||||
tmp = EcoAccount()
|
|
||||||
identifier = tmp.generate_new_identifier()
|
|
||||||
while EcoAccount.objects.filter(identifier=identifier).exists():
|
|
||||||
identifier = tmp.generate_new_identifier()
|
|
||||||
return JsonResponse(
|
|
||||||
data={
|
|
||||||
"gen_data": identifier
|
|
||||||
}
|
}
|
||||||
)
|
context = BaseContext(request, context).context
|
||||||
|
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
|
class EcoAccountIdentifierGeneratorView(AbstractIdentifierGeneratorView):
|
||||||
|
_MODEL = EcoAccount
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class EditEcoAccountView(LoginRequiredMixin, View):
|
||||||
@default_group_required
|
_TEMPLATE = "compensation/form/view.html"
|
||||||
@shared_access_required(EcoAccount, "id")
|
|
||||||
def edit_view(request: HttpRequest, id: str):
|
|
||||||
"""
|
|
||||||
Renders a view for editing compensations
|
|
||||||
|
|
||||||
Args:
|
@method_decorator(default_group_required)
|
||||||
request (HttpRequest): The incoming request
|
@method_decorator(shared_access_required(EcoAccount, "id"))
|
||||||
|
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
|
||||||
Returns:
|
"""
|
||||||
|
Renders a view for editing compensations
|
||||||
|
|
||||||
"""
|
Args:
|
||||||
template = "compensation/form/view.html"
|
request (HttpRequest): The incoming request
|
||||||
# Get object from db
|
|
||||||
acc = get_object_or_404(EcoAccount, id=id)
|
Returns:
|
||||||
if acc.is_recorded:
|
|
||||||
messages.info(
|
"""
|
||||||
request,
|
# Get object from db
|
||||||
RECORDED_BLOCKS_EDIT
|
acc = get_object_or_404(EcoAccount, id=id)
|
||||||
)
|
if acc.is_recorded:
|
||||||
return redirect("compensation:acc:detail", id=id)
|
messages.info(
|
||||||
|
request,
|
||||||
|
RECORDED_BLOCKS_EDIT
|
||||||
|
)
|
||||||
|
return redirect("compensation:acc:detail", id=id)
|
||||||
|
|
||||||
|
# Create forms, initialize with values from db/from POST request
|
||||||
|
data_form = EditEcoAccountForm(request.POST or None, instance=acc)
|
||||||
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"form": data_form,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
|
@method_decorator(default_group_required)
|
||||||
|
@method_decorator(shared_access_required(EcoAccount, "id"))
|
||||||
|
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Renders a view for editing compensations
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
# Get object from db
|
||||||
|
acc = get_object_or_404(EcoAccount, id=id)
|
||||||
|
if acc.is_recorded:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
RECORDED_BLOCKS_EDIT
|
||||||
|
)
|
||||||
|
return redirect("compensation:acc:detail", id=id)
|
||||||
|
|
||||||
|
# Create forms, initialize with values from db/from POST request
|
||||||
|
data_form = EditEcoAccountForm(request.POST or None, instance=acc)
|
||||||
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
|
||||||
|
|
||||||
# Create forms, initialize with values from db/from POST request
|
|
||||||
data_form = EditEcoAccountForm(request.POST or None, instance=acc)
|
|
||||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
|
|
||||||
if request.method == "POST":
|
|
||||||
data_form_valid = data_form.is_valid()
|
data_form_valid = data_form.is_valid()
|
||||||
geom_form_valid = geom_form.is_valid()
|
geom_form_valid = geom_form.is_valid()
|
||||||
if data_form_valid and geom_form_valid:
|
if data_form_valid and geom_form_valid:
|
||||||
# The data form takes the geom form for processing, as well as the performing user
|
# The data form takes the geom form for processing, as well as the performing user
|
||||||
acc = data_form.save(request.user, geom_form)
|
acc = data_form.save(request.user, geom_form)
|
||||||
messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
|
messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
|
||||||
if geom_form.geometry_simplified:
|
if geom_form.has_geometry_simplified():
|
||||||
messages.info(
|
messages.info(
|
||||||
request,
|
request,
|
||||||
GEOMETRY_SIMPLIFIED
|
GEOMETRY_SIMPLIFIED
|
||||||
)
|
)
|
||||||
|
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||||
|
if num_ignored_geometries > 0:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||||
|
)
|
||||||
return redirect("compensation:acc:detail", id=acc.id)
|
return redirect("compensation:acc:detail", id=acc.id)
|
||||||
else:
|
else:
|
||||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
messages.error(request, FORM_INVALID, extra_tags="danger", )
|
||||||
else:
|
|
||||||
# For clarification: nothing in this case
|
|
||||||
pass
|
|
||||||
context = {
|
|
||||||
"form": data_form,
|
|
||||||
"geom_form": geom_form,
|
|
||||||
TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier),
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"form": data_form,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
|
||||||
@login_required
|
return render(request, self._TEMPLATE, context)
|
||||||
@any_group_check
|
|
||||||
@uuid_required
|
|
||||||
def detail_view(request: HttpRequest, id: str):
|
|
||||||
""" Renders a detail view for a compensation
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (HttpRequest): The incoming request
|
|
||||||
id (str): The compensation's id
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
template = "compensation/detail/eco_account/view.html"
|
|
||||||
acc = get_object_or_404(
|
|
||||||
EcoAccount.objects.prefetch_related(
|
|
||||||
"deadlines",
|
|
||||||
).select_related(
|
|
||||||
'geometry',
|
|
||||||
'responsible',
|
|
||||||
),
|
|
||||||
id=id,
|
|
||||||
deleted=None,
|
|
||||||
)
|
|
||||||
geom_form = SimpleGeomForm(instance=acc)
|
|
||||||
parcels = acc.get_underlying_parcels()
|
|
||||||
_user = request.user
|
|
||||||
is_data_shared = acc.is_shared_with(_user)
|
|
||||||
|
|
||||||
# Order states according to surface
|
|
||||||
before_states = acc.before_states.order_by("-surface")
|
|
||||||
after_states = acc.after_states.order_by("-surface")
|
|
||||||
|
|
||||||
# Precalculate logical errors between before- and after-states
|
|
||||||
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
|
|
||||||
sum_before_states = acc.get_surface_before_states()
|
|
||||||
sum_after_states = acc.get_surface_after_states()
|
|
||||||
diff_states = abs(sum_before_states - sum_after_states)
|
|
||||||
# Calculate rest of available surface for deductions
|
|
||||||
available_total = acc.deductable_rest
|
|
||||||
available_relative = acc.get_deductable_rest_relative()
|
|
||||||
|
|
||||||
# Prefetch related data to decrease the amount of db connections
|
|
||||||
deductions = acc.deductions.filter(
|
|
||||||
intervention__deleted=None,
|
|
||||||
)
|
|
||||||
actions = acc.actions.all()
|
|
||||||
|
|
||||||
request = acc.set_status_messages(request)
|
|
||||||
|
|
||||||
requesting_user_is_only_shared_user = acc.is_only_shared_with(_user)
|
|
||||||
if requesting_user_is_only_shared_user:
|
|
||||||
messages.info(
|
|
||||||
request,
|
|
||||||
DO_NOT_FORGET_TO_SHARE
|
|
||||||
)
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"obj": acc,
|
|
||||||
"geom_form": geom_form,
|
|
||||||
"parcels": parcels,
|
|
||||||
"has_access": is_data_shared,
|
|
||||||
"before_states": before_states,
|
|
||||||
"after_states": after_states,
|
|
||||||
"sum_before_states": sum_before_states,
|
|
||||||
"sum_after_states": sum_after_states,
|
|
||||||
"diff_states": diff_states,
|
|
||||||
"available": available_relative,
|
|
||||||
"available_total": available_total,
|
|
||||||
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
|
||||||
"is_zb_member": _user.in_group(ZB_GROUP),
|
|
||||||
"is_ets_member": _user.in_group(ETS_GROUP),
|
|
||||||
"LANIS_LINK": acc.get_LANIS_link(),
|
|
||||||
"deductions": deductions,
|
|
||||||
"actions": actions,
|
|
||||||
TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}",
|
|
||||||
"has_finished_deadlines": acc.get_finished_deadlines().exists(),
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required_modal
|
|
||||||
@login_required
|
|
||||||
@default_group_required
|
|
||||||
@shared_access_required(EcoAccount, "id")
|
|
||||||
def remove_view(request: HttpRequest, id: str):
|
|
||||||
""" Renders a modal view for removing the eco account
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (HttpRequest): The incoming request
|
|
||||||
id (str): The account's id
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
acc = get_object_or_404(EcoAccount, id=id)
|
|
||||||
|
|
||||||
# If the eco account has already been recorded OR there are already deductions, it can not be deleted by a regular
|
|
||||||
# default group user
|
|
||||||
if acc.recorded is not None or acc.deductions.exists():
|
|
||||||
user = request.user
|
|
||||||
if not user.in_group(ETS_GROUP):
|
|
||||||
messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED)
|
|
||||||
return redirect("compensation:acc:detail", id=id)
|
|
||||||
|
|
||||||
form = RemoveEcoAccountModalForm(request.POST or None, instance=acc, request=request)
|
|
||||||
return form.process_request(
|
|
||||||
request=request,
|
|
||||||
msg_success=_("Eco-account removed"),
|
|
||||||
redirect_url=reverse("compensation:acc:index"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|||||||
22
compensation/views/eco_account/remove.py
Normal file
22
compensation/views/eco_account/remove.py
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Created on: 14.12.25
|
||||||
|
|
||||||
|
"""
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from compensation.forms.eco_account import RemoveEcoAccountModalForm
|
||||||
|
from compensation.models import EcoAccount
|
||||||
|
from konova.decorators import shared_access_required
|
||||||
|
from konova.views.remove import AbstractRemoveView
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveEcoAccountView(AbstractRemoveView):
|
||||||
|
_MODEL = EcoAccount
|
||||||
|
_REDIRECT_URL = "compensation:acc:index"
|
||||||
|
_FORM = RemoveEcoAccountModalForm
|
||||||
|
|
||||||
|
@method_decorator(shared_access_required(EcoAccount, "id"))
|
||||||
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
@@ -5,7 +5,7 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
|||||||
Created on: 19.08.22
|
Created on: 19.08.22
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import 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 _
|
||||||
@@ -14,74 +14,79 @@ from compensation.models import EcoAccount
|
|||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
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.qrcode import QrCode
|
||||||
|
from konova.views.report import AbstractPublicReportView
|
||||||
|
|
||||||
|
|
||||||
def report_view(request: HttpRequest, id: str):
|
class EcoAccountPublicReportView(AbstractPublicReportView):
|
||||||
""" Renders the public report view
|
_TEMPLATE = "compensation/report/eco_account/report.html"
|
||||||
|
|
||||||
Args:
|
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
request (HttpRequest): The incoming request
|
""" Renders the public report view
|
||||||
id (str): The id of the intervention
|
|
||||||
|
|
||||||
Returns:
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): The id of the intervention
|
||||||
|
|
||||||
"""
|
Returns:
|
||||||
# Reuse the compensation report template since EcoAccounts are structurally identical
|
|
||||||
template = "compensation/report/eco_account/report.html"
|
"""
|
||||||
acc = get_object_or_404(EcoAccount, id=id)
|
acc = get_object_or_404(EcoAccount, id=id)
|
||||||
|
tab_title = _("Report {}").format(acc.identifier)
|
||||||
|
# If intervention is not recorded (yet or currently) we need to render another template without any data
|
||||||
|
if not acc.is_ready_for_publish():
|
||||||
|
template = "report/unavailable.html"
|
||||||
|
context = {
|
||||||
|
TAB_TITLE_IDENTIFIER: tab_title,
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, template, context)
|
||||||
|
|
||||||
|
# Prepare data for map viewer
|
||||||
|
geom_form = SimpleGeomForm(
|
||||||
|
instance=acc
|
||||||
|
)
|
||||||
|
parcels = acc.get_underlying_parcels()
|
||||||
|
|
||||||
|
qrcode = QrCode(
|
||||||
|
content=request.build_absolute_uri(reverse("compensation:acc:report", args=(id,))),
|
||||||
|
size=10
|
||||||
|
)
|
||||||
|
qrcode_lanis = QrCode(
|
||||||
|
content=acc.get_LANIS_link(),
|
||||||
|
size=7
|
||||||
|
)
|
||||||
|
|
||||||
|
# Order states by surface
|
||||||
|
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent")
|
||||||
|
after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent")
|
||||||
|
actions = acc.actions.all().prefetch_related("action_type__parent")
|
||||||
|
|
||||||
|
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier)
|
||||||
|
deductions = acc.deductions.all() \
|
||||||
|
.distinct("intervention") \
|
||||||
|
.select_related("intervention") \
|
||||||
|
.values_list("intervention__id", "intervention__identifier", "intervention__title", named=True)
|
||||||
|
|
||||||
tab_title = _("Report {}").format(acc.identifier)
|
|
||||||
# If intervention is not recorded (yet or currently) we need to render another template without any data
|
|
||||||
if not acc.is_ready_for_publish():
|
|
||||||
template = "report/unavailable.html"
|
|
||||||
context = {
|
context = {
|
||||||
|
"obj": acc,
|
||||||
|
"qrcode": {
|
||||||
|
"img": qrcode.get_img(),
|
||||||
|
"url": qrcode.get_content(),
|
||||||
|
},
|
||||||
|
"qrcode_lanis": {
|
||||||
|
"img": qrcode_lanis.get_img(),
|
||||||
|
"url": qrcode_lanis.get_content(),
|
||||||
|
},
|
||||||
|
"is_entry_shared": False, # disables action buttons during rendering
|
||||||
|
"before_states": before_states,
|
||||||
|
"after_states": after_states,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
"parcels": parcels,
|
||||||
|
"actions": actions,
|
||||||
|
"deductions": deductions,
|
||||||
|
"tables_scrollable": False,
|
||||||
TAB_TITLE_IDENTIFIER: tab_title,
|
TAB_TITLE_IDENTIFIER: tab_title,
|
||||||
}
|
}
|
||||||
context = BaseContext(request, context).context
|
context = BaseContext(request, context).context
|
||||||
return render(request, template, context)
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
# Prepare data for map viewer
|
|
||||||
geom_form = SimpleGeomForm(
|
|
||||||
instance=acc
|
|
||||||
)
|
|
||||||
parcels = acc.get_underlying_parcels()
|
|
||||||
|
|
||||||
qrcode_url = request.build_absolute_uri(reverse("compensation:acc:report", args=(id,)))
|
|
||||||
qrcode_img = generate_qr_code(qrcode_url, 10)
|
|
||||||
qrcode_lanis_url = acc.get_LANIS_link()
|
|
||||||
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
|
|
||||||
|
|
||||||
# Order states by surface
|
|
||||||
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent")
|
|
||||||
after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent")
|
|
||||||
actions = acc.actions.all().prefetch_related("action_type__parent")
|
|
||||||
|
|
||||||
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier)
|
|
||||||
deductions = acc.deductions.all()\
|
|
||||||
.distinct("intervention")\
|
|
||||||
.select_related("intervention")\
|
|
||||||
.values_list("intervention__id", "intervention__identifier", "intervention__title", named=True)
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"obj": acc,
|
|
||||||
"qrcode": {
|
|
||||||
"img": qrcode_img,
|
|
||||||
"url": qrcode_url,
|
|
||||||
},
|
|
||||||
"qrcode_lanis": {
|
|
||||||
"img": qrcode_img_lanis,
|
|
||||||
"url": qrcode_lanis_url,
|
|
||||||
},
|
|
||||||
"has_access": False, # disables action buttons during rendering
|
|
||||||
"before_states": before_states,
|
|
||||||
"after_states": after_states,
|
|
||||||
"geom_form": geom_form,
|
|
||||||
"parcels": parcels,
|
|
||||||
"actions": actions,
|
|
||||||
"deductions": deductions,
|
|
||||||
"tables_scrollable": False,
|
|
||||||
TAB_TITLE_IDENTIFIER: tab_title,
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|||||||
@@ -16,4 +16,5 @@ class EmaAdmin(AbstractCompensationAdmin):
|
|||||||
"teams",
|
"teams",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
admin.site.register(Ema, EmaAdmin)
|
admin.site.register(Ema, EmaAdmin)
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-action' obj.id %}" title="{% trans 'Add new action' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-action' obj.id %}" title="{% trans 'Add new action' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'seedling' %}
|
{% fa5_icon 'seedling' %}
|
||||||
@@ -49,7 +49,7 @@
|
|||||||
<hr>
|
<hr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% for detail in action.action_type_details.all %}
|
{% for detail in action.action_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'ema:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
|
<button data-form-url="{% url 'ema:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -11,14 +11,14 @@
|
|||||||
{% fa5_icon 'file-alt' %}
|
{% fa5_icon 'file-alt' %}
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}">
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}">
|
||||||
{% fa5_icon 'bell' %}
|
{% fa5_icon 'bell' %}
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-form' obj.id %}">
|
|
||||||
{% fa5_icon 'share-alt' %}
|
|
||||||
</button>
|
|
||||||
{% if is_ets_member %}
|
{% if is_ets_member %}
|
||||||
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-form' obj.id %}">
|
||||||
|
{% fa5_icon 'share-alt' %}
|
||||||
|
</button>
|
||||||
{% if obj.recorded %}
|
{% if obj.recorded %}
|
||||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'ema:record' obj.id %}">
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'ema:record' obj.id %}">
|
||||||
{% fa5_icon 'bookmark' 'far' %}
|
{% fa5_icon 'bookmark' 'far' %}
|
||||||
@@ -28,19 +28,21 @@
|
|||||||
{% fa5_icon 'bookmark' %}
|
{% fa5_icon 'bookmark' %}
|
||||||
</button>
|
</button>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
<a href="{% url 'ema:edit' obj.id %}" class="mr-2">
|
||||||
|
<button class="btn btn-default" title="{% trans 'Edit' %}">
|
||||||
|
{% fa5_icon 'edit' %}
|
||||||
|
</button>
|
||||||
|
</a>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if is_default_member %}
|
{% if is_default_member %}
|
||||||
<a href="{% url 'ema:edit' obj.id %}" class="mr-2">
|
<button class="btn btn-default btn-modal mr-2" data-form-url="{% url 'ema:log' obj.id %}" title="{% trans 'Show log' %}">
|
||||||
<button class="btn btn-default" title="{% trans 'Edit' %}">
|
{% fa5_icon 'history' %}
|
||||||
{% fa5_icon 'edit' %}
|
</button>
|
||||||
|
{% endif %}
|
||||||
|
{% if is_ets_member %}
|
||||||
|
<button class="btn btn-default btn-modal" data-form-url="{% url 'ema:remove' obj.id %}" title="{% trans 'Delete' %}">
|
||||||
|
{% fa5_icon 'trash' %}
|
||||||
</button>
|
</button>
|
||||||
</a>
|
|
||||||
<button class="btn btn-default btn-modal mr-2" data-form-url="{% url 'ema:log' obj.id %}" title="{% trans 'Show log' %}">
|
|
||||||
{% fa5_icon 'history' %}
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-default btn-modal" data-form-url="{% url 'ema:remove' obj.id %}" title="{% trans 'Delete' %}">
|
|
||||||
{% fa5_icon 'trash' %}
|
|
||||||
</button>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'calendar-check' %}
|
{% fa5_icon 'calendar-check' %}
|
||||||
@@ -58,7 +58,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'ema:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
|
<button data-form-url="{% url 'ema:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'file' %}
|
{% fa5_icon 'file' %}
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'ema:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
<button data-form-url="{% url 'ema:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'layer-group' %}
|
{% fa5_icon 'layer-group' %}
|
||||||
@@ -49,14 +49,14 @@
|
|||||||
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
<br>
|
<br>
|
||||||
{% for detail in state.biotope_type_details.all %}
|
{% for detail in state.biotope_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'layer-group' %}
|
{% fa5_icon 'layer-group' %}
|
||||||
@@ -49,14 +49,14 @@
|
|||||||
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
|
||||||
<br>
|
<br>
|
||||||
{% for detail in state.biotope_type_details.all %}
|
{% for detail in state.biotope_type_details.all %}
|
||||||
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
|
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
|
||||||
{% empty %}
|
{% empty %}
|
||||||
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
</td>
|
</td>
|
||||||
<td>{{ state.surface|floatformat:2 }} m²</td>
|
<td>{{ state.surface|floatformat:2 }} m²</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -87,7 +87,7 @@
|
|||||||
{% include 'user/includes/team_data_modal_button.html' %}
|
{% include 'user/includes/team_data_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<hr>
|
<hr>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
{% for user in obj.users.all %}
|
{% for user in obj.users.all %}
|
||||||
{% include 'user/includes/contact_modal_button.html' %}
|
{% include 'user/includes/contact_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -118,6 +118,7 @@ class EmaViewTestCase(CompensationViewTestCase):
|
|||||||
self.index_url,
|
self.index_url,
|
||||||
self.detail_url,
|
self.detail_url,
|
||||||
self.report_url,
|
self.report_url,
|
||||||
|
self.log_url,
|
||||||
]
|
]
|
||||||
fail_urls = [
|
fail_urls = [
|
||||||
self.new_url,
|
self.new_url,
|
||||||
@@ -133,7 +134,6 @@ class EmaViewTestCase(CompensationViewTestCase):
|
|||||||
self.action_remove_url,
|
self.action_remove_url,
|
||||||
self.action_new_url,
|
self.action_new_url,
|
||||||
self.new_doc_url,
|
self.new_doc_url,
|
||||||
self.log_url,
|
|
||||||
self.remove_url,
|
self.remove_url,
|
||||||
]
|
]
|
||||||
self.assert_url_fail(client, fail_urls)
|
self.assert_url_fail(client, fail_urls)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
post_data = {
|
post_data = {
|
||||||
"identifier": test_id,
|
"identifier": test_id,
|
||||||
"title": test_title,
|
"title": test_title,
|
||||||
"geom": geom_json,
|
"output": geom_json,
|
||||||
"conservation_office": test_conservation_office.id
|
"conservation_office": test_conservation_office.id
|
||||||
}
|
}
|
||||||
self.client_user.post(new_url, post_data)
|
self.client_user.post(new_url, post_data)
|
||||||
@@ -84,7 +84,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
new_title = self.create_dummy_string()
|
new_title = self.create_dummy_string()
|
||||||
new_identifier = self.create_dummy_string()
|
new_identifier = self.create_dummy_string()
|
||||||
new_comment = self.create_dummy_string()
|
new_comment = self.create_dummy_string()
|
||||||
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
|
new_geometry = self.create_dummy_geometry() # Create an empty geometry
|
||||||
test_conservation_office = self.get_conservation_office_code()
|
test_conservation_office = self.get_conservation_office_code()
|
||||||
|
|
||||||
check_on_elements = {
|
check_on_elements = {
|
||||||
@@ -99,7 +99,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
"identifier": new_identifier,
|
"identifier": new_identifier,
|
||||||
"title": new_title,
|
"title": new_title,
|
||||||
"comment": new_comment,
|
"comment": new_comment,
|
||||||
"geom": new_geometry.geojson,
|
"output": self.create_geojson(new_geometry),
|
||||||
"conservation_office": test_conservation_office.id
|
"conservation_office": test_conservation_office.id
|
||||||
}
|
}
|
||||||
self.client_user.post(url, post_data)
|
self.client_user.post(url, post_data)
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ class NewEmaFormTestCase(BaseTestCase):
|
|||||||
)
|
)
|
||||||
geom_form_data = json.loads(geom_form_data)
|
geom_form_data = json.loads(geom_form_data)
|
||||||
geom_form_data = {
|
geom_form_data = {
|
||||||
"geom": json.dumps(geom_form_data)
|
"output": json.dumps(geom_form_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
geom_form = SimpleGeomForm(geom_form_data)
|
geom_form = SimpleGeomForm(geom_form_data)
|
||||||
@@ -116,7 +116,7 @@ class EditEmaFormTestCase(BaseTestCase):
|
|||||||
)
|
)
|
||||||
geom_form_data = json.loads(geom_form_data)
|
geom_form_data = json.loads(geom_form_data)
|
||||||
geom_form_data = {
|
geom_form_data = {
|
||||||
"geom": json.dumps(geom_form_data)
|
"output": json.dumps(geom_form_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
geom_form = SimpleGeomForm(geom_form_data)
|
geom_form = SimpleGeomForm(geom_form_data)
|
||||||
|
|||||||
20
ema/urls.py
20
ema/urls.py
@@ -9,26 +9,28 @@ from django.urls import path
|
|||||||
|
|
||||||
from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView
|
from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView
|
||||||
from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView
|
from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView
|
||||||
|
from ema.views.detail import DetailEmaView
|
||||||
from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView
|
from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView
|
||||||
from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_view, remove_view
|
from ema.views.ema import IndexEmaView, EmaIdentifierGeneratorView, EditEmaView, NewEmaView
|
||||||
from ema.views.log import EmaLogView
|
from ema.views.log import EmaLogView
|
||||||
from ema.views.record import EmaRecordView
|
from ema.views.record import EmaRecordView
|
||||||
from ema.views.report import report_view
|
from ema.views.remove import RemoveEmaView
|
||||||
|
from ema.views.report import EmaPublicReportView
|
||||||
from ema.views.resubmission import EmaResubmissionView
|
from ema.views.resubmission import EmaResubmissionView
|
||||||
from ema.views.share import EmaShareFormView, EmaShareByTokenView
|
from ema.views.share import EmaShareFormView, EmaShareByTokenView
|
||||||
from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView
|
from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView
|
||||||
|
|
||||||
app_name = "ema"
|
app_name = "ema"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", index_view, name="index"),
|
path("", IndexEmaView.as_view(), name="index"),
|
||||||
path("new/", new_view, name="new"),
|
path("new/", NewEmaView.as_view(), name="new"),
|
||||||
path("new/id", new_id_view, name="new-id"),
|
path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"),
|
||||||
path("<id>", detail_view, name="detail"),
|
path("<id>", DetailEmaView.as_view(), name="detail"),
|
||||||
path('<id>/log', EmaLogView.as_view(), name='log'),
|
path('<id>/log', EmaLogView.as_view(), name='log'),
|
||||||
path('<id>/edit', edit_view, name='edit'),
|
path('<id>/edit', EditEmaView.as_view(), name='edit'),
|
||||||
path('<id>/remove', remove_view, name='remove'),
|
path('<id>/remove', RemoveEmaView.as_view(), name='remove'),
|
||||||
path('<id>/record', EmaRecordView.as_view(), name='record'),
|
path('<id>/record', EmaRecordView.as_view(), name='record'),
|
||||||
path('<id>/report', report_view, name='report'),
|
path('<id>/report', EmaPublicReportView.as_view(), name='report'),
|
||||||
path('<id>/resub', EmaResubmissionView.as_view(), name='resubmission-create'),
|
path('<id>/resub', EmaResubmissionView.as_view(), name='resubmission-create'),
|
||||||
|
|
||||||
path('<id>/state/new', NewEmaStateView.as_view(), name='new-state'),
|
path('<id>/state/new', NewEmaStateView.as_view(), name='new-state'),
|
||||||
|
|||||||
76
ema/views/detail.py
Normal file
76
ema/views/detail.py
Normal file
@@ -0,0 +1,76 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Created on: 14.12.25
|
||||||
|
|
||||||
|
"""
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.http import HttpResponse, HttpRequest
|
||||||
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
|
||||||
|
from ema.models import Ema
|
||||||
|
from konova.contexts import BaseContext
|
||||||
|
from konova.forms import SimpleGeomForm
|
||||||
|
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
||||||
|
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||||
|
from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE
|
||||||
|
from konova.views.detail import AbstractDetailView
|
||||||
|
|
||||||
|
|
||||||
|
class DetailEmaView(AbstractDetailView):
|
||||||
|
_TEMPLATE = "ema/detail/view.html"
|
||||||
|
|
||||||
|
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
""" Renders the detail view of an EMA
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): The EMA id
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
ema = get_object_or_404(Ema, id=id, deleted=None)
|
||||||
|
|
||||||
|
geom_form = SimpleGeomForm(instance=ema)
|
||||||
|
parcels = ema.get_underlying_parcels()
|
||||||
|
_user = request.user
|
||||||
|
is_entry_shared = ema.is_shared_with(_user)
|
||||||
|
|
||||||
|
# Order states according to surface
|
||||||
|
before_states = ema.before_states.all().order_by("-surface")
|
||||||
|
after_states = ema.after_states.all().order_by("-surface")
|
||||||
|
|
||||||
|
# Precalculate logical errors between before- and after-states
|
||||||
|
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
|
||||||
|
sum_before_states = ema.get_surface_before_states()
|
||||||
|
sum_after_states = ema.get_surface_after_states()
|
||||||
|
diff_states = abs(sum_before_states - sum_after_states)
|
||||||
|
|
||||||
|
ema.set_status_messages(request)
|
||||||
|
|
||||||
|
requesting_user_is_only_shared_user = ema.is_only_shared_with(_user)
|
||||||
|
if requesting_user_is_only_shared_user:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
DO_NOT_FORGET_TO_SHARE
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"obj": ema,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
"parcels": parcels,
|
||||||
|
"is_entry_shared": is_entry_shared,
|
||||||
|
"before_states": before_states,
|
||||||
|
"after_states": after_states,
|
||||||
|
"sum_before_states": sum_before_states,
|
||||||
|
"sum_after_states": sum_after_states,
|
||||||
|
"diff_states": diff_states,
|
||||||
|
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
||||||
|
"is_zb_member": _user.in_group(ZB_GROUP),
|
||||||
|
"is_ets_member": _user.in_group(ETS_GROUP),
|
||||||
|
"LANIS_LINK": ema.get_LANIS_link(),
|
||||||
|
TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}",
|
||||||
|
"has_finished_deadlines": ema.get_finished_deadlines().exists(),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
341
ema/views/ema.py
341
ema/views/ema.py
@@ -7,71 +7,96 @@ Created on: 19.08.22
|
|||||||
"""
|
"""
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.db.models import Sum
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http import HttpRequest, JsonResponse
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect, render
|
from django.shortcuts import get_object_or_404, redirect, render
|
||||||
from django.urls import reverse
|
from django.utils.decorators import method_decorator
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views.generic.base import View
|
||||||
|
|
||||||
from ema.forms import NewEmaForm, EditEmaForm
|
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
|
||||||
uuid_required
|
|
||||||
from konova.forms import SimpleGeomForm
|
from konova.forms import SimpleGeomForm
|
||||||
from konova.forms.modals import RemoveModalForm
|
|
||||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
|
||||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||||
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
|
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
|
||||||
DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
|
GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
|
||||||
|
from konova.views.identifier import AbstractIdentifierGeneratorView
|
||||||
|
from konova.views.index import AbstractIndexView
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class IndexEmaView(AbstractIndexView):
|
||||||
def index_view(request: HttpRequest):
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
""" Renders the index view for EMAs
|
""" Renders the index view for EMAs
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request (HttpRequest): The incoming request
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
template = "generic_index.html"
|
emas = Ema.objects.filter(
|
||||||
emas = Ema.objects.filter(
|
deleted=None,
|
||||||
deleted=None,
|
).order_by(
|
||||||
).order_by(
|
"-modified__timestamp"
|
||||||
"-modified__timestamp"
|
)
|
||||||
)
|
|
||||||
|
|
||||||
table = EmaTable(
|
table = EmaTable(
|
||||||
request,
|
request,
|
||||||
queryset=emas
|
queryset=emas
|
||||||
)
|
)
|
||||||
context = {
|
context = {
|
||||||
"table": table,
|
"table": table,
|
||||||
TAB_TITLE_IDENTIFIER: _("EMAs - Overview"),
|
TAB_TITLE_IDENTIFIER: _("EMAs - Overview"),
|
||||||
}
|
}
|
||||||
context = BaseContext(request, context).context
|
context = BaseContext(request, context).context
|
||||||
return render(request, template, context)
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
|
class NewEmaView(LoginRequiredMixin, View):
|
||||||
|
_TEMPLATE = "ema/form/view.html"
|
||||||
|
|
||||||
@login_required
|
@method_decorator(conservation_office_group_required)
|
||||||
@conservation_office_group_required
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
def new_view(request: HttpRequest):
|
""" GET endpoint
|
||||||
"""
|
|
||||||
Renders a view for a new eco account creation
|
|
||||||
|
|
||||||
Args:
|
Renders form for new EMA
|
||||||
request (HttpRequest): The incoming request
|
|
||||||
|
|
||||||
Returns:
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
*args ():
|
||||||
|
**kwargs ():
|
||||||
|
|
||||||
"""
|
Returns:
|
||||||
template = "ema/form/view.html"
|
|
||||||
data_form = NewEmaForm(request.POST or None)
|
"""
|
||||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
data_form = NewEmaForm(request.POST or None)
|
||||||
if request.method == "POST":
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
||||||
|
context = {
|
||||||
|
"form": data_form,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
TAB_TITLE_IDENTIFIER: _("New EMA"),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
|
@method_decorator(conservation_office_group_required)
|
||||||
|
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
""" POST endpoint
|
||||||
|
|
||||||
|
Processes submitted form
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
*args ():
|
||||||
|
**kwargs ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
data_form = NewEmaForm(request.POST or None)
|
||||||
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
||||||
if data_form.is_valid() and geom_form.is_valid():
|
if data_form.is_valid() and geom_form.is_valid():
|
||||||
generated_identifier = data_form.cleaned_data.get("identifier", None)
|
generated_identifier = data_form.cleaned_data.get("identifier", None)
|
||||||
ema = data_form.save(request.user, geom_form)
|
ema = data_form.save(request.user, geom_form)
|
||||||
@@ -84,176 +109,124 @@ def new_view(request: HttpRequest):
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
messages.success(request, _("EMA {} added").format(ema.identifier))
|
messages.success(request, _("EMA {} added").format(ema.identifier))
|
||||||
if geom_form.geometry_simplified:
|
if geom_form.has_geometry_simplified():
|
||||||
messages.info(
|
messages.info(
|
||||||
request,
|
request,
|
||||||
GEOMETRY_SIMPLIFIED
|
GEOMETRY_SIMPLIFIED
|
||||||
)
|
)
|
||||||
|
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||||
|
if num_ignored_geometries > 0:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||||
|
)
|
||||||
return redirect("ema:detail", id=ema.id)
|
return redirect("ema:detail", id=ema.id)
|
||||||
else:
|
else:
|
||||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
||||||
else:
|
context = {
|
||||||
# For clarification: nothing in this case
|
"form": data_form,
|
||||||
pass
|
"geom_form": geom_form,
|
||||||
context = {
|
TAB_TITLE_IDENTIFIER: _("New EMA"),
|
||||||
"form": data_form,
|
|
||||||
"geom_form": geom_form,
|
|
||||||
TAB_TITLE_IDENTIFIER: _("New EMA"),
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@conservation_office_group_required
|
|
||||||
def new_id_view(request: HttpRequest):
|
|
||||||
""" JSON endpoint
|
|
||||||
|
|
||||||
Provides fetching of free identifiers for e.g. AJAX calls
|
|
||||||
|
|
||||||
"""
|
|
||||||
tmp = Ema()
|
|
||||||
identifier = tmp.generate_new_identifier()
|
|
||||||
while Ema.objects.filter(identifier=identifier).exists():
|
|
||||||
identifier = tmp.generate_new_identifier()
|
|
||||||
return JsonResponse(
|
|
||||||
data={
|
|
||||||
"gen_data": identifier
|
|
||||||
}
|
}
|
||||||
)
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
|
class EmaIdentifierGeneratorView(AbstractIdentifierGeneratorView):
|
||||||
|
_MODEL = Ema
|
||||||
|
|
||||||
@login_required
|
@method_decorator(conservation_office_group_required)
|
||||||
@uuid_required
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
def detail_view(request: HttpRequest, id: str):
|
return super().get(request, *args, **kwargs)
|
||||||
""" Renders the detail view of an EMA
|
|
||||||
|
|
||||||
Args:
|
class EditEmaView(LoginRequiredMixin, View):
|
||||||
request (HttpRequest): The incoming request
|
_TEMPLATE = "compensation/form/view.html"
|
||||||
id (str): The EMA id
|
|
||||||
|
|
||||||
Returns:
|
@method_decorator(conservation_office_group_required)
|
||||||
|
@method_decorator(shared_access_required(Ema, "id"))
|
||||||
|
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
""" GET endpoint
|
||||||
|
|
||||||
"""
|
Renders form
|
||||||
template = "ema/detail/view.html"
|
|
||||||
ema = get_object_or_404(Ema, id=id, deleted=None)
|
|
||||||
|
|
||||||
geom_form = SimpleGeomForm(instance=ema)
|
Args:
|
||||||
parcels = ema.get_underlying_parcels()
|
request (HttpRequest): The incoming request
|
||||||
_user = request.user
|
id (str): The ema identifier
|
||||||
is_data_shared = ema.is_shared_with(_user)
|
*args ():
|
||||||
|
**kwargs ():
|
||||||
|
|
||||||
# Order states according to surface
|
Returns:
|
||||||
before_states = ema.before_states.all().order_by("-surface")
|
|
||||||
after_states = ema.after_states.all().order_by("-surface")
|
|
||||||
|
|
||||||
# Precalculate logical errors between before- and after-states
|
"""
|
||||||
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
|
# Get object from db
|
||||||
sum_before_states = ema.get_surface_before_states()
|
ema = get_object_or_404(Ema, id=id)
|
||||||
sum_after_states = ema.get_surface_after_states()
|
if ema.is_recorded:
|
||||||
diff_states = abs(sum_before_states - sum_after_states)
|
messages.info(
|
||||||
|
request,
|
||||||
|
RECORDED_BLOCKS_EDIT
|
||||||
|
)
|
||||||
|
return redirect("ema:detail", id=id)
|
||||||
|
|
||||||
ema.set_status_messages(request)
|
# Create forms, initialize with values from db/from POST request
|
||||||
|
data_form = EditEmaForm(instance=ema)
|
||||||
|
geom_form = SimpleGeomForm(read_only=False, instance=ema)
|
||||||
|
context = {
|
||||||
|
"form": data_form,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
requesting_user_is_only_shared_user = ema.is_only_shared_with(_user)
|
@method_decorator(conservation_office_group_required)
|
||||||
if requesting_user_is_only_shared_user:
|
@method_decorator(shared_access_required(Ema, "id"))
|
||||||
messages.info(
|
def post(self, request: HttpRequest, id:str, *args, **kwargs) -> HttpResponse:
|
||||||
request,
|
""" POST endpoint
|
||||||
DO_NOT_FORGET_TO_SHARE
|
|
||||||
)
|
|
||||||
|
|
||||||
context = {
|
Process submitted forms
|
||||||
"obj": ema,
|
|
||||||
"geom_form": geom_form,
|
|
||||||
"parcels": parcels,
|
|
||||||
"has_access": is_data_shared,
|
|
||||||
"before_states": before_states,
|
|
||||||
"after_states": after_states,
|
|
||||||
"sum_before_states": sum_before_states,
|
|
||||||
"sum_after_states": sum_after_states,
|
|
||||||
"diff_states": diff_states,
|
|
||||||
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
|
||||||
"is_zb_member": _user.in_group(ZB_GROUP),
|
|
||||||
"is_ets_member": _user.in_group(ETS_GROUP),
|
|
||||||
"LANIS_LINK": ema.get_LANIS_link(),
|
|
||||||
TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}",
|
|
||||||
"has_finished_deadlines": ema.get_finished_deadlines().exists(),
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): The id of the ema
|
||||||
|
*args ():
|
||||||
|
**kwargs ():
|
||||||
|
|
||||||
@login_required
|
Returns:
|
||||||
@conservation_office_group_required
|
|
||||||
@shared_access_required(Ema, "id")
|
|
||||||
def edit_view(request: HttpRequest, id: str):
|
|
||||||
"""
|
|
||||||
Renders a view for editing compensations
|
|
||||||
|
|
||||||
Args:
|
"""
|
||||||
request (HttpRequest): The incoming request
|
# Get object from db
|
||||||
|
ema = get_object_or_404(Ema, id=id)
|
||||||
|
if ema.is_recorded:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
RECORDED_BLOCKS_EDIT
|
||||||
|
)
|
||||||
|
return redirect("ema:detail", id=id)
|
||||||
|
|
||||||
Returns:
|
# Create forms, initialize with values from db/from POST request
|
||||||
|
data_form = EditEmaForm(request.POST or None, instance=ema)
|
||||||
"""
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema)
|
||||||
template = "compensation/form/view.html"
|
|
||||||
# Get object from db
|
|
||||||
ema = get_object_or_404(Ema, id=id)
|
|
||||||
if ema.is_recorded:
|
|
||||||
messages.info(
|
|
||||||
request,
|
|
||||||
RECORDED_BLOCKS_EDIT
|
|
||||||
)
|
|
||||||
return redirect("ema:detail", id=id)
|
|
||||||
|
|
||||||
# Create forms, initialize with values from db/from POST request
|
|
||||||
data_form = EditEmaForm(request.POST or None, instance=ema)
|
|
||||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema)
|
|
||||||
if request.method == "POST":
|
|
||||||
if data_form.is_valid() and geom_form.is_valid():
|
if data_form.is_valid() and geom_form.is_valid():
|
||||||
# The data form takes the geom form for processing, as well as the performing user
|
# The data form takes the geom form for processing, as well as the performing user
|
||||||
ema = data_form.save(request.user, geom_form)
|
ema = data_form.save(request.user, geom_form)
|
||||||
messages.success(request, _("EMA {} edited").format(ema.identifier))
|
messages.success(request, _("EMA {} edited").format(ema.identifier))
|
||||||
if geom_form.geometry_simplified:
|
if geom_form.has_geometry_simplified():
|
||||||
messages.info(
|
messages.info(
|
||||||
request,
|
request,
|
||||||
GEOMETRY_SIMPLIFIED
|
GEOMETRY_SIMPLIFIED
|
||||||
)
|
)
|
||||||
|
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||||
|
if num_ignored_geometries > 0:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||||
|
)
|
||||||
return redirect("ema:detail", id=ema.id)
|
return redirect("ema:detail", id=ema.id)
|
||||||
else:
|
else:
|
||||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
messages.error(request, FORM_INVALID, extra_tags="danger", )
|
||||||
else:
|
context = {
|
||||||
# For clarification: nothing in this case
|
"form": data_form,
|
||||||
pass
|
"geom_form": geom_form,
|
||||||
context = {
|
TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier),
|
||||||
"form": data_form,
|
}
|
||||||
"geom_form": geom_form,
|
context = BaseContext(request, context).context
|
||||||
TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier),
|
return render(request, self._TEMPLATE, context)
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required_modal
|
|
||||||
@login_required
|
|
||||||
@conservation_office_group_required
|
|
||||||
@shared_access_required(Ema, "id")
|
|
||||||
def remove_view(request: HttpRequest, id: str):
|
|
||||||
""" Renders a modal view for removing the EMA
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (HttpRequest): The incoming request
|
|
||||||
id (str): The EMA's id
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
ema = get_object_or_404(Ema, id=id)
|
|
||||||
form = RemoveModalForm(request.POST or None, instance=ema, request=request)
|
|
||||||
return form.process_request(
|
|
||||||
request=request,
|
|
||||||
msg_success=_("EMA removed"),
|
|
||||||
redirect_url=reverse("ema:index"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ class EmaLogView(AbstractLogView):
|
|||||||
|
|
||||||
@method_decorator(login_required_modal)
|
@method_decorator(login_required_modal)
|
||||||
@method_decorator(login_required)
|
@method_decorator(login_required)
|
||||||
@method_decorator(conservation_office_group_required)
|
|
||||||
@method_decorator(shared_access_required(Ema, "id"))
|
@method_decorator(shared_access_required(Ema, "id"))
|
||||||
def dispatch(self, request, *args, **kwargs):
|
def dispatch(self, request, *args, **kwargs):
|
||||||
return super().dispatch(request, *args, **kwargs)
|
return super().dispatch(request, *args, **kwargs)
|
||||||
|
|||||||
21
ema/views/remove.py
Normal file
21
ema/views/remove.py
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Created on: 14.12.25
|
||||||
|
|
||||||
|
"""
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from ema.models import Ema
|
||||||
|
from konova.decorators import shared_access_required, conservation_office_group_required
|
||||||
|
from konova.views.remove import AbstractRemoveView
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveEmaView(AbstractRemoveView):
|
||||||
|
_MODEL = Ema
|
||||||
|
_REDIRECT_URL = "ema:index"
|
||||||
|
|
||||||
|
@method_decorator(conservation_office_group_required)
|
||||||
|
@method_decorator(shared_access_required(Ema, "id"))
|
||||||
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
@@ -5,7 +5,7 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
|||||||
Created on: 19.08.22
|
Created on: 19.08.22
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import 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 _
|
||||||
@@ -14,67 +14,72 @@ from ema.models import Ema
|
|||||||
from konova.contexts import BaseContext
|
from konova.contexts import BaseContext
|
||||||
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.qrcode import QrCode
|
||||||
|
from konova.views.report import AbstractPublicReportView
|
||||||
|
|
||||||
|
|
||||||
def report_view(request:HttpRequest, id: str):
|
class EmaPublicReportView(AbstractPublicReportView):
|
||||||
""" Renders the public report view
|
_TEMPLATE = "ema/report/report.html"
|
||||||
|
|
||||||
Args:
|
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
request (HttpRequest): The incoming request
|
""" Renders the public report view
|
||||||
id (str): The id of the intervention
|
|
||||||
|
|
||||||
Returns:
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): The id of the intervention
|
||||||
|
|
||||||
"""
|
Returns:
|
||||||
# Reuse the compensation report template since EMAs are structurally identical
|
|
||||||
template = "ema/report/report.html"
|
"""
|
||||||
ema = get_object_or_404(Ema, id=id)
|
ema = get_object_or_404(Ema, id=id)
|
||||||
|
tab_title = _("Report {}").format(ema.identifier)
|
||||||
|
# If intervention is not recorded (yet or currently) we need to render another template without any data
|
||||||
|
if not ema.is_ready_for_publish():
|
||||||
|
template = "report/unavailable.html"
|
||||||
|
context = {
|
||||||
|
TAB_TITLE_IDENTIFIER: tab_title,
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, template, context)
|
||||||
|
|
||||||
|
# Prepare data for map viewer
|
||||||
|
geom_form = SimpleGeomForm(
|
||||||
|
instance=ema,
|
||||||
|
)
|
||||||
|
parcels = ema.get_underlying_parcels()
|
||||||
|
|
||||||
|
qrcode = QrCode(
|
||||||
|
content=request.build_absolute_uri(reverse("ema:report", args=(id,))),
|
||||||
|
size=10
|
||||||
|
)
|
||||||
|
qrcode_lanis = QrCode(
|
||||||
|
content=ema.get_LANIS_link(),
|
||||||
|
size=7
|
||||||
|
)
|
||||||
|
|
||||||
|
# Order states by surface
|
||||||
|
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||||
|
after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||||
|
actions = ema.actions.all().prefetch_related("action_type")
|
||||||
|
|
||||||
tab_title = _("Report {}").format(ema.identifier)
|
|
||||||
# If intervention is not recorded (yet or currently) we need to render another template without any data
|
|
||||||
if not ema.is_ready_for_publish():
|
|
||||||
template = "report/unavailable.html"
|
|
||||||
context = {
|
context = {
|
||||||
|
"obj": ema,
|
||||||
|
"qrcode": {
|
||||||
|
"img": qrcode.get_img(),
|
||||||
|
"url": qrcode.get_content(),
|
||||||
|
},
|
||||||
|
"qrcode_lanis": {
|
||||||
|
"img": qrcode_lanis.get_img(),
|
||||||
|
"url": qrcode_lanis.get_content(),
|
||||||
|
},
|
||||||
|
"is_entry_shared": False, # disables action buttons during rendering
|
||||||
|
"before_states": before_states,
|
||||||
|
"after_states": after_states,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
"parcels": parcels,
|
||||||
|
"actions": actions,
|
||||||
|
"tables_scrollable": False,
|
||||||
TAB_TITLE_IDENTIFIER: tab_title,
|
TAB_TITLE_IDENTIFIER: tab_title,
|
||||||
}
|
}
|
||||||
context = BaseContext(request, context).context
|
context = BaseContext(request, context).context
|
||||||
return render(request, template, context)
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
# Prepare data for map viewer
|
|
||||||
geom_form = SimpleGeomForm(
|
|
||||||
instance=ema,
|
|
||||||
)
|
|
||||||
parcels = ema.get_underlying_parcels()
|
|
||||||
|
|
||||||
qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
|
|
||||||
qrcode_img = generate_qr_code(qrcode_url, 10)
|
|
||||||
qrcode_lanis_url = ema.get_LANIS_link()
|
|
||||||
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
|
|
||||||
|
|
||||||
# Order states by surface
|
|
||||||
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type")
|
|
||||||
after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type")
|
|
||||||
actions = ema.actions.all().prefetch_related("action_type")
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"obj": ema,
|
|
||||||
"qrcode": {
|
|
||||||
"img": qrcode_img,
|
|
||||||
"url": qrcode_url
|
|
||||||
},
|
|
||||||
"qrcode_lanis": {
|
|
||||||
"img": qrcode_img_lanis,
|
|
||||||
"url": qrcode_lanis_url
|
|
||||||
},
|
|
||||||
"has_access": False, # disables action buttons during rendering
|
|
||||||
"before_states": before_states,
|
|
||||||
"after_states": after_states,
|
|
||||||
"geom_form": geom_form,
|
|
||||||
"parcels": parcels,
|
|
||||||
"actions": actions,
|
|
||||||
"tables_scrollable": False,
|
|
||||||
TAB_TITLE_IDENTIFIER: tab_title,
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
@@ -37,6 +37,14 @@ class InterventionAdmin(BaseObjectAdmin):
|
|||||||
"geometry",
|
"geometry",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def get_actions(self, request):
|
||||||
|
DELETE_ACTION_IDENTIFIER = "delete_selected"
|
||||||
|
actions = super().get_actions(request)
|
||||||
|
|
||||||
|
if DELETE_ACTION_IDENTIFIER in actions:
|
||||||
|
del actions[DELETE_ACTION_IDENTIFIER]
|
||||||
|
|
||||||
|
return actions
|
||||||
|
|
||||||
class InterventionDocumentAdmin(AbstractDocumentAdmin):
|
class InterventionDocumentAdmin(AbstractDocumentAdmin):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -5,6 +5,8 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
|||||||
Created on: 30.11.20
|
Created on: 30.11.20
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
from konova.sub_settings.django_settings import env
|
||||||
|
|
||||||
INTERVENTION_IDENTIFIER_LENGTH = 6
|
INTERVENTION_IDENTIFIER_LENGTH = 6
|
||||||
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}"
|
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}"
|
||||||
|
|
||||||
@@ -14,7 +16,7 @@ INTERVENTION_LANIS_LAYER_NAME_UNRECORDED_OLD_ENTRY = "eiv_unrecorded_old_entries
|
|||||||
|
|
||||||
# EGON connection settings via rabbitmq
|
# EGON connection settings via rabbitmq
|
||||||
# NEEDED FOR BACKWARDS COMPATIBILITY
|
# NEEDED FOR BACKWARDS COMPATIBILITY
|
||||||
EGON_RABBITMQ_HOST = "CHANGE_ME"
|
EGON_RABBITMQ_HOST = env("EGON_RABBITMQ_HOST")
|
||||||
EGON_RABBITMQ_PORT = "CHANGE_ME"
|
EGON_RABBITMQ_PORT = env("EGON_RABBITMQ_PORT")
|
||||||
EGON_RABBITMQ_USER = "CHANGE_ME"
|
EGON_RABBITMQ_USER = env("EGON_RABBITMQ_USER")
|
||||||
EGON_RABBITMQ_PW = "CHANGE_ME"
|
EGON_RABBITMQ_PW = env("EGON_RABBITMQ_PW")
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<a href="{% url 'compensation:new' obj.id %}" title="{% trans 'Add new compensation' %}">
|
<a href="{% url 'compensation:new' obj.id %}" title="{% trans 'Add new compensation' %}">
|
||||||
<button class="btn btn-outline-default">
|
<button class="btn btn-outline-default">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Title' %}
|
{% trans 'Title' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@@ -51,7 +51,7 @@
|
|||||||
</td>
|
</td>
|
||||||
<td class="align-middle">{{ comp.title }}</td>
|
<td class="align-middle">{{ comp.title }}</td>
|
||||||
<td>
|
<td>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'intervention:remove-compensation' obj.id comp.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove compensation' %}">
|
<button data-form-url="{% url 'intervention:remove-compensation' obj.id comp.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove compensation' %}">
|
||||||
{% fa5_icon 'trash' %}
|
{% fa5_icon 'trash' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
{% fa5_icon 'file-alt' %}
|
{% fa5_icon 'file-alt' %}
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'intervention:resubmission-create' obj.id %}">
|
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'intervention:resubmission-create' obj.id %}">
|
||||||
{% fa5_icon 'bell' %}
|
{% fa5_icon 'bell' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'tree' %}
|
{% fa5_icon 'tree' %}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Created' %}
|
{% trans 'Created' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@@ -56,7 +56,7 @@
|
|||||||
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
|
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
|
||||||
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
|
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'intervention:edit-deduction' obj.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
|
<button data-form-url="{% url 'intervention:edit-deduction' obj.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'file' %}
|
{% fa5_icon 'file' %}
|
||||||
@@ -38,7 +38,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@@ -66,7 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'intervention:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
<button data-form-url="{% url 'intervention:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="col-sm-6">
|
<div class="col-sm-6">
|
||||||
<div class="d-flex justify-content-end">
|
<div class="d-flex justify-content-end">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:pay:new' obj.id %}" title="{% trans 'Add new payment' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:pay:new' obj.id %}" title="{% trans 'Add new payment' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'money-bill-wave' %}
|
{% fa5_icon 'money-bill-wave' %}
|
||||||
@@ -33,7 +33,7 @@
|
|||||||
<th class="w-50" scope="col">
|
<th class="w-50" scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@@ -46,16 +46,24 @@
|
|||||||
{% for pay in obj.payments.all %}
|
{% for pay in obj.payments.all %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="align-middle">
|
<td class="align-middle">
|
||||||
{{ pay.amount|floatformat:2 }} €
|
{% if is_entry_shared %}
|
||||||
|
{{ 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">
|
||||||
{{ pay.comment }}
|
{% if is_entry_shared %}
|
||||||
|
{{ pay.comment }}
|
||||||
|
{% else %}
|
||||||
|
{% trans 'This data is not shared with you' %}
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'compensation:pay:edit' obj.id pay.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit payment' %}">
|
<button data-form-url="{% url 'compensation:pay:edit' obj.id pay.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit payment' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
{% comment %}
|
{% comment %}
|
||||||
Only show add-button if no revocation exists, yet.
|
Only show add-button if no revocation exists, yet.
|
||||||
{% endcomment %}
|
{% endcomment %}
|
||||||
{% if is_default_member and has_access and not obj.legal.revocation %}
|
{% if is_default_member and is_entry_shared and not obj.legal.revocation %}
|
||||||
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-revocation' obj.id %}" title="{% trans 'Add revocation' %}">
|
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-revocation' obj.id %}" title="{% trans 'Add revocation' %}">
|
||||||
{% fa5_icon 'plus' %}
|
{% fa5_icon 'plus' %}
|
||||||
{% fa5_icon 'ban' %}
|
{% fa5_icon 'ban' %}
|
||||||
@@ -36,7 +36,7 @@
|
|||||||
<th scope="col">
|
<th scope="col">
|
||||||
{% trans 'Comment' %}
|
{% trans 'Comment' %}
|
||||||
</th>
|
</th>
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<th class="w-10" scope="col">
|
<th class="w-10" scope="col">
|
||||||
<span class="float-right">
|
<span class="float-right">
|
||||||
{% trans 'Action' %}
|
{% trans 'Action' %}
|
||||||
@@ -64,7 +64,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="align-middle float-right">
|
<td class="align-middle float-right">
|
||||||
{% if is_default_member and has_access %}
|
{% if is_default_member and is_entry_shared %}
|
||||||
<button data-form-url="{% url 'intervention:edit-revocation' obj.id rev.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit revocation' %}">
|
<button data-form-url="{% url 'intervention:edit-revocation' obj.id rev.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit revocation' %}">
|
||||||
{% fa5_icon 'edit' %}
|
{% fa5_icon 'edit' %}
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -129,7 +129,7 @@
|
|||||||
{% include 'user/includes/team_data_modal_button.html' %}
|
{% include 'user/includes/team_data_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
<hr>
|
<hr>
|
||||||
{% if has_access %}
|
{% if is_entry_shared %}
|
||||||
{% for user in obj.users.all %}
|
{% for user in obj.users.all %}
|
||||||
{% include 'user/includes/contact_modal_button.html' %}
|
{% include 'user/includes/contact_modal_button.html' %}
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
|
|||||||
post_data = {
|
post_data = {
|
||||||
"identifier": test_id,
|
"identifier": test_id,
|
||||||
"title": test_title,
|
"title": test_title,
|
||||||
"geom": geom_json,
|
"output": geom_json,
|
||||||
}
|
}
|
||||||
response = self.client_user.post(
|
response = self.client_user.post(
|
||||||
new_url,
|
new_url,
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ class NewInterventionFormTestCase(BaseTestCase):
|
|||||||
)
|
)
|
||||||
geom_form_data = json.loads(geom_form_data)
|
geom_form_data = json.loads(geom_form_data)
|
||||||
geom_form_data = {
|
geom_form_data = {
|
||||||
"geom": json.dumps(geom_form_data)
|
"output": json.dumps(geom_form_data)
|
||||||
}
|
}
|
||||||
geom_form = SimpleGeomForm(geom_form_data)
|
geom_form = SimpleGeomForm(geom_form_data)
|
||||||
|
|
||||||
@@ -104,7 +104,7 @@ class EditInterventionFormTestCase(NewInterventionFormTestCase):
|
|||||||
)
|
)
|
||||||
geom_form_data = json.loads(geom_form_data)
|
geom_form_data = json.loads(geom_form_data)
|
||||||
geom_form_data = {
|
geom_form_data = {
|
||||||
"geom": json.dumps(geom_form_data)
|
"output": json.dumps(geom_form_data)
|
||||||
}
|
}
|
||||||
|
|
||||||
geom_form = SimpleGeomForm(geom_form_data)
|
geom_form = SimpleGeomForm(geom_form_data)
|
||||||
@@ -124,7 +124,7 @@ class EditInterventionFormTestCase(NewInterventionFormTestCase):
|
|||||||
self.assertIsNotNone(obj.responsible.handler)
|
self.assertIsNotNone(obj.responsible.handler)
|
||||||
self.assertEqual(obj.title, data["title"])
|
self.assertEqual(obj.title, data["title"])
|
||||||
self.assertEqual(obj.comment, data["comment"])
|
self.assertEqual(obj.comment, data["comment"])
|
||||||
self.assertTrue(test_geom.equals_exact(obj.geometry.geom, 0.000001))
|
self.assert_equal_geometries(test_geom, obj.geometry.geom)
|
||||||
|
|
||||||
self.assertEqual(obj.legal.binding_date, today)
|
self.assertEqual(obj.legal.binding_date, today)
|
||||||
self.assertEqual(obj.legal.registration_date, today)
|
self.assertEqual(obj.legal.registration_date, today)
|
||||||
|
|||||||
@@ -8,39 +8,42 @@ Created on: 30.11.20
|
|||||||
from django.urls import path
|
from django.urls import path
|
||||||
|
|
||||||
from intervention.autocomplete.intervention import InterventionAutocomplete
|
from intervention.autocomplete.intervention import InterventionAutocomplete
|
||||||
from intervention.views.check import check_view
|
from intervention.views.check import InterventionCheckView
|
||||||
from intervention.views.compensation import remove_compensation_view
|
from intervention.views.compensation import RemoveCompensationFromInterventionView
|
||||||
from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \
|
from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \
|
||||||
RemoveInterventionDeductionView
|
RemoveInterventionDeductionView
|
||||||
from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \
|
from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \
|
||||||
RemoveInterventionDocumentView, EditInterventionDocumentView
|
RemoveInterventionDocumentView, EditInterventionDocumentView
|
||||||
from intervention.views.intervention import index_view, new_view, new_id_view, detail_view, edit_view, remove_view
|
from intervention.views.intervention import IndexInterventionView, InterventionIdentifierGeneratorView, \
|
||||||
|
NewInterventionView, EditInterventionView
|
||||||
|
from intervention.views.remove import RemoveInterventionView
|
||||||
|
from intervention.views.detail import DetailInterventionView
|
||||||
from intervention.views.log import InterventionLogView
|
from intervention.views.log import InterventionLogView
|
||||||
from intervention.views.record import InterventionRecordView
|
from intervention.views.record import InterventionRecordView
|
||||||
from intervention.views.report import report_view
|
from intervention.views.report import InterventionPublicReportView
|
||||||
from intervention.views.resubmission import InterventionResubmissionView
|
from intervention.views.resubmission import InterventionResubmissionView
|
||||||
from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \
|
from intervention.views.revocation import NewInterventionRevocationView, GetInterventionRevocationView, \
|
||||||
get_revocation_view
|
EditInterventionRevocationView, RemoveInterventionRevocationView
|
||||||
from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView
|
from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView
|
||||||
|
|
||||||
app_name = "intervention"
|
app_name = "intervention"
|
||||||
urlpatterns = [
|
urlpatterns = [
|
||||||
path("", index_view, name="index"),
|
path("", IndexInterventionView.as_view(), name="index"),
|
||||||
path('new/', new_view, name='new'),
|
path('new/', NewInterventionView.as_view(), name='new'),
|
||||||
path('new/id', new_id_view, name='new-id'),
|
path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'),
|
||||||
path('<id>', detail_view, name='detail'),
|
path('<id>', DetailInterventionView.as_view(), name='detail'),
|
||||||
path('<id>/log', InterventionLogView.as_view(), name='log'),
|
path('<id>/log', InterventionLogView.as_view(), name='log'),
|
||||||
path('<id>/edit', edit_view, name='edit'),
|
path('<id>/edit', EditInterventionView.as_view(), name='edit'),
|
||||||
path('<id>/remove', remove_view, name='remove'),
|
path('<id>/remove', RemoveInterventionView.as_view(), name='remove'),
|
||||||
path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'),
|
path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'),
|
||||||
path('<id>/share', InterventionShareFormView.as_view(), name='share-form'),
|
path('<id>/share', InterventionShareFormView.as_view(), name='share-form'),
|
||||||
path('<id>/check', check_view, name='check'),
|
path('<id>/check', InterventionCheckView.as_view(), name='check'),
|
||||||
path('<id>/record', InterventionRecordView.as_view(), name='record'),
|
path('<id>/record', InterventionRecordView.as_view(), name='record'),
|
||||||
path('<id>/report', report_view, name='report'),
|
path('<id>/report', InterventionPublicReportView.as_view(), name='report'),
|
||||||
path('<id>/resub', InterventionResubmissionView.as_view(), name='resubmission-create'),
|
path('<id>/resub', InterventionResubmissionView.as_view(), name='resubmission-create'),
|
||||||
|
|
||||||
# Compensations
|
# Compensations
|
||||||
path('<id>/compensation/<comp_id>/remove', remove_compensation_view, name='remove-compensation'),
|
path('<id>/compensation/<comp_id>/remove', RemoveCompensationFromInterventionView.as_view(), name='remove-compensation'),
|
||||||
|
|
||||||
# Documents
|
# Documents
|
||||||
path('<id>/document/new/', NewInterventionDocumentView.as_view(), name='new-doc'),
|
path('<id>/document/new/', NewInterventionDocumentView.as_view(), name='new-doc'),
|
||||||
@@ -54,10 +57,10 @@ urlpatterns = [
|
|||||||
path('<id>/deduction/<deduction_id>/remove', RemoveInterventionDeductionView.as_view(), name='remove-deduction'),
|
path('<id>/deduction/<deduction_id>/remove', RemoveInterventionDeductionView.as_view(), name='remove-deduction'),
|
||||||
|
|
||||||
# Revocation routes
|
# Revocation routes
|
||||||
path('<id>/revocation/new', new_revocation_view, name='new-revocation'),
|
path('<id>/revocation/new', NewInterventionRevocationView.as_view(), name='new-revocation'),
|
||||||
path('<id>/revocation/<revocation_id>/edit', edit_revocation_view, name='edit-revocation'),
|
path('<id>/revocation/<revocation_id>/edit', EditInterventionRevocationView.as_view(), name='edit-revocation'),
|
||||||
path('<id>/revocation/<revocation_id>/remove', remove_revocation_view, name='remove-revocation'),
|
path('<id>/revocation/<revocation_id>/remove', RemoveInterventionRevocationView.as_view(), name='remove-revocation'),
|
||||||
path('revocation/<doc_id>', get_revocation_view, name='get-doc-revocation'),
|
path('revocation/<doc_id>', GetInterventionRevocationView.as_view(), name='get-doc-revocation'),
|
||||||
|
|
||||||
# Autocomplete
|
# Autocomplete
|
||||||
path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="autocomplete"),
|
path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="autocomplete"),
|
||||||
|
|||||||
@@ -5,35 +5,44 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
|||||||
Created on: 19.08.22
|
Created on: 19.08.22
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
from intervention.forms.modals.check import CheckModalForm
|
from intervention.forms.modals.check import CheckModalForm
|
||||||
from intervention.models import Intervention
|
from intervention.models import Intervention
|
||||||
from konova.decorators import registration_office_group_required, shared_access_required
|
from konova.decorators import registration_office_group_required, shared_access_required
|
||||||
from konova.utils.message_templates import INTERVENTION_INVALID
|
from konova.utils.message_templates import INTERVENTION_INVALID
|
||||||
|
|
||||||
|
class InterventionCheckView(LoginRequiredMixin, View):
|
||||||
|
|
||||||
@login_required
|
def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
@registration_office_group_required
|
""" Renders check form for an intervention
|
||||||
@shared_access_required(Intervention, "id")
|
|
||||||
def check_view(request: HttpRequest, id: str):
|
|
||||||
""" Renders check form for an intervention
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request (HttpRequest): The incoming request
|
request (HttpRequest): The incoming request
|
||||||
id (str): Intervention's id
|
id (str): Intervention's id
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
intervention = get_object_or_404(Intervention, id=id)
|
intervention = get_object_or_404(Intervention, id=id)
|
||||||
form = CheckModalForm(request.POST or None, instance=intervention, request=request)
|
form = CheckModalForm(request.POST or None, instance=intervention, request=request)
|
||||||
return form.process_request(
|
return form.process_request(
|
||||||
request,
|
request,
|
||||||
msg_success=_("Check performed"),
|
msg_success=_("Check performed"),
|
||||||
msg_error=INTERVENTION_INVALID
|
msg_error=INTERVENTION_INVALID
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@method_decorator(registration_office_group_required)
|
||||||
|
@method_decorator(shared_access_required(Intervention, "id"))
|
||||||
|
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
return self.__process_request(request, id, *args, **kwargs)
|
||||||
|
|
||||||
|
@method_decorator(registration_office_group_required)
|
||||||
|
@method_decorator(shared_access_required(Intervention, "id"))
|
||||||
|
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
return self.__process_request(request, id, *args, **kwargs)
|
||||||
|
|||||||
@@ -5,42 +5,50 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
|||||||
Created on: 19.08.22
|
Created on: 19.08.22
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.http import HttpRequest, Http404
|
from django.http import HttpRequest, Http404, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404
|
from django.shortcuts import get_object_or_404
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
from intervention.models import Intervention
|
from intervention.models import Intervention
|
||||||
from konova.decorators import shared_access_required, login_required_modal
|
from konova.decorators import shared_access_required
|
||||||
from konova.forms.modals import RemoveModalForm
|
from konova.forms.modals import RemoveModalForm
|
||||||
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
|
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
|
||||||
|
|
||||||
|
|
||||||
@login_required_modal
|
class RemoveCompensationFromInterventionView(LoginRequiredMixin, View):
|
||||||
@login_required
|
|
||||||
@shared_access_required(Intervention, "id")
|
|
||||||
def remove_compensation_view(request: HttpRequest, id: str, comp_id: str):
|
|
||||||
""" Renders a modal view for removing the compensation
|
|
||||||
|
|
||||||
Args:
|
def __process_request(self, request: HttpRequest, id: str, comp_id: str, *args, **kwargs) -> HttpResponse:
|
||||||
request (HttpRequest): The incoming request
|
""" Renders a modal view for removing the compensation
|
||||||
id (str): The compensation's id
|
|
||||||
|
|
||||||
Returns:
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): The compensation's id
|
||||||
|
|
||||||
"""
|
Returns:
|
||||||
intervention = get_object_or_404(Intervention, id=id)
|
|
||||||
try:
|
"""
|
||||||
comp = intervention.compensations.get(
|
intervention = get_object_or_404(Intervention, id=id)
|
||||||
id=comp_id
|
try:
|
||||||
|
comp = intervention.compensations.get(
|
||||||
|
id=comp_id
|
||||||
|
)
|
||||||
|
except ObjectDoesNotExist:
|
||||||
|
raise Http404("Unknown compensation")
|
||||||
|
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
|
||||||
|
return form.process_request(
|
||||||
|
request=request,
|
||||||
|
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
|
||||||
|
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data",
|
||||||
)
|
)
|
||||||
except ObjectDoesNotExist:
|
|
||||||
raise Http404("Unknown compensation")
|
|
||||||
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
|
|
||||||
return form.process_request(
|
|
||||||
request=request,
|
|
||||||
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
|
|
||||||
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
@method_decorator(shared_access_required(Intervention, "id"))
|
||||||
|
def get(self, request, id: str, comp_id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
return self.__process_request(request, id, comp_id, *args, **kwargs)
|
||||||
|
|
||||||
|
@method_decorator(shared_access_required(Intervention, "id"))
|
||||||
|
def post(self, request, id: str, comp_id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
return self.__process_request(request, id, comp_id, *args, **kwargs)
|
||||||
79
intervention/views/detail.py
Normal file
79
intervention/views/detail.py
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Created on: 14.12.25
|
||||||
|
|
||||||
|
"""
|
||||||
|
from django.contrib import messages
|
||||||
|
from django.http import HttpResponse
|
||||||
|
from django.shortcuts import get_object_or_404, render
|
||||||
|
|
||||||
|
from intervention.models import Intervention
|
||||||
|
from konova.contexts import BaseContext
|
||||||
|
from konova.forms import SimpleGeomForm
|
||||||
|
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
||||||
|
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||||
|
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, DO_NOT_FORGET_TO_SHARE
|
||||||
|
from konova.views.detail import AbstractDetailView
|
||||||
|
|
||||||
|
|
||||||
|
class DetailInterventionView(AbstractDetailView):
|
||||||
|
_TEMPLATE = "intervention/detail/view.html"
|
||||||
|
|
||||||
|
def get(self, request, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
# Fetch data, filter out deleted related data
|
||||||
|
intervention = get_object_or_404(
|
||||||
|
Intervention.objects.select_related(
|
||||||
|
"geometry",
|
||||||
|
"legal",
|
||||||
|
"responsible",
|
||||||
|
).prefetch_related(
|
||||||
|
"legal__revocations",
|
||||||
|
),
|
||||||
|
id=id,
|
||||||
|
deleted=None
|
||||||
|
)
|
||||||
|
compensations = intervention.compensations.filter(
|
||||||
|
deleted=None,
|
||||||
|
)
|
||||||
|
_user = request.user
|
||||||
|
is_data_shared = intervention.is_shared_with(user=_user)
|
||||||
|
|
||||||
|
geom_form = SimpleGeomForm(
|
||||||
|
instance=intervention,
|
||||||
|
)
|
||||||
|
last_checked = intervention.get_last_checked_action()
|
||||||
|
last_checked_tooltip = ""
|
||||||
|
if last_checked:
|
||||||
|
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
|
||||||
|
last_checked.get_timestamp_str_formatted(),
|
||||||
|
last_checked.user
|
||||||
|
)
|
||||||
|
|
||||||
|
has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists()
|
||||||
|
|
||||||
|
requesting_user_is_only_shared_user = intervention.is_only_shared_with(_user)
|
||||||
|
if requesting_user_is_only_shared_user:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
DO_NOT_FORGET_TO_SHARE
|
||||||
|
)
|
||||||
|
|
||||||
|
context = {
|
||||||
|
"obj": intervention,
|
||||||
|
"last_checked": last_checked,
|
||||||
|
"last_checked_tooltip": last_checked_tooltip,
|
||||||
|
"compensations": compensations,
|
||||||
|
"is_entry_shared": is_data_shared,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
||||||
|
"is_zb_member": _user.in_group(ZB_GROUP),
|
||||||
|
"is_ets_member": _user.in_group(ETS_GROUP),
|
||||||
|
"LANIS_LINK": intervention.get_LANIS_link(),
|
||||||
|
"has_payment_without_document": has_payment_without_document,
|
||||||
|
TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}",
|
||||||
|
}
|
||||||
|
|
||||||
|
request = intervention.set_status_messages(request)
|
||||||
|
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
@@ -7,75 +7,98 @@ Created on: 19.08.22
|
|||||||
"""
|
"""
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.decorators import login_required
|
||||||
from django.http import JsonResponse, HttpRequest
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, render, redirect
|
from django.shortcuts import get_object_or_404, render, redirect
|
||||||
from django.urls import reverse
|
from django.utils.decorators import method_decorator
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
from intervention.forms.intervention import EditInterventionForm, NewInterventionForm
|
from intervention.forms.intervention import EditInterventionForm, NewInterventionForm
|
||||||
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
|
||||||
uuid_required
|
|
||||||
from konova.forms import SimpleGeomForm
|
from konova.forms import SimpleGeomForm
|
||||||
from konova.forms.modals import RemoveModalForm
|
|
||||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
|
||||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||||
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \
|
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, \
|
||||||
CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
|
CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, \
|
||||||
|
GEOMETRIES_IGNORED_TEMPLATE
|
||||||
|
from konova.views.identifier import AbstractIdentifierGeneratorView
|
||||||
|
from konova.views.index import AbstractIndexView
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class IndexInterventionView(AbstractIndexView):
|
||||||
@any_group_check
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
def index_view(request: HttpRequest):
|
"""
|
||||||
"""
|
Renders the index view for Interventions
|
||||||
Renders the index view for Interventions
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request (HttpRequest): The incoming request
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
A rendered view
|
A rendered view
|
||||||
"""
|
"""
|
||||||
template = "generic_index.html"
|
# Filtering by user access is performed in table filter inside InterventionTableFilter class
|
||||||
|
interventions = Intervention.objects.filter(
|
||||||
# Filtering by user access is performed in table filter inside InterventionTableFilter class
|
deleted=None, # not deleted
|
||||||
interventions = Intervention.objects.filter(
|
).select_related(
|
||||||
deleted=None, # not deleted
|
"legal"
|
||||||
).select_related(
|
).order_by(
|
||||||
"legal"
|
"-modified__timestamp"
|
||||||
).order_by(
|
)
|
||||||
"-modified__timestamp"
|
table = InterventionTable(
|
||||||
)
|
request=request,
|
||||||
table = InterventionTable(
|
queryset=interventions
|
||||||
request=request,
|
)
|
||||||
queryset=interventions
|
context = {
|
||||||
)
|
"table": table,
|
||||||
context = {
|
TAB_TITLE_IDENTIFIER: _("Interventions - Overview"),
|
||||||
"table": table,
|
}
|
||||||
TAB_TITLE_IDENTIFIER: _("Interventions - Overview"),
|
context = BaseContext(request, context).context
|
||||||
}
|
return render(request, self._TEMPLATE, context)
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class NewInterventionView(LoginRequiredMixin, View):
|
||||||
@default_group_required
|
_TEMPLATE = "intervention/form/view.html"
|
||||||
def new_view(request: HttpRequest):
|
|
||||||
"""
|
|
||||||
Renders a view for a new intervention creation
|
|
||||||
|
|
||||||
Args:
|
@method_decorator(default_group_required)
|
||||||
request (HttpRequest): The incoming request
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
|
||||||
Returns:
|
"""
|
||||||
|
Renders a view for a new intervention creation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
data_form = NewInterventionForm()
|
||||||
|
geom_form = SimpleGeomForm(read_only=False)
|
||||||
|
context = {
|
||||||
|
"form": data_form,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
TAB_TITLE_IDENTIFIER: _("New intervention"),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
|
@method_decorator(default_group_required)
|
||||||
|
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
|
||||||
|
"""
|
||||||
|
Renders a view for a new intervention creation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
data_form = NewInterventionForm(request.POST or None)
|
||||||
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
||||||
|
|
||||||
"""
|
|
||||||
template = "intervention/form/view.html"
|
|
||||||
data_form = NewInterventionForm(request.POST or None)
|
|
||||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
|
||||||
if request.method == "POST":
|
|
||||||
if data_form.is_valid() and geom_form.is_valid():
|
if data_form.is_valid() and geom_form.is_valid():
|
||||||
generated_identifier = data_form.cleaned_data.get("identifier", None)
|
generated_identifier = data_form.cleaned_data.get("identifier", None)
|
||||||
intervention = data_form.save(request.user, geom_form)
|
intervention = data_form.save(request.user, geom_form)
|
||||||
@@ -87,147 +110,100 @@ def new_view(request: HttpRequest):
|
|||||||
intervention.identifier
|
intervention.identifier
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
messages.success(request, _("Intervention {} added").format(intervention.identifier))
|
messages.success(request, _("Intervention {} added").format(intervention.identifier))
|
||||||
if geom_form.geometry_simplified:
|
if geom_form.has_geometry_simplified():
|
||||||
messages.info(
|
messages.info(
|
||||||
request,
|
request,
|
||||||
GEOMETRY_SIMPLIFIED
|
GEOMETRY_SIMPLIFIED
|
||||||
)
|
)
|
||||||
|
|
||||||
|
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||||
|
if num_ignored_geometries > 0:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||||
|
)
|
||||||
return redirect("intervention:detail", id=intervention.id)
|
return redirect("intervention:detail", id=intervention.id)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
messages.error(request, FORM_INVALID, extra_tags="danger", )
|
||||||
else:
|
context = {
|
||||||
# For clarification: nothing in this case
|
"form": data_form,
|
||||||
pass
|
"geom_form": geom_form,
|
||||||
context = {
|
TAB_TITLE_IDENTIFIER: _("New intervention"),
|
||||||
"form": data_form,
|
|
||||||
"geom_form": geom_form,
|
|
||||||
TAB_TITLE_IDENTIFIER: _("New intervention"),
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@default_group_required
|
|
||||||
def new_id_view(request: HttpRequest):
|
|
||||||
""" JSON endpoint
|
|
||||||
|
|
||||||
Provides fetching of free identifiers for e.g. AJAX calls
|
|
||||||
|
|
||||||
"""
|
|
||||||
tmp_intervention = Intervention()
|
|
||||||
identifier = tmp_intervention.generate_new_identifier()
|
|
||||||
while Intervention.objects.filter(identifier=identifier).exists():
|
|
||||||
identifier = tmp_intervention.generate_new_identifier()
|
|
||||||
return JsonResponse(
|
|
||||||
data={
|
|
||||||
"gen_data": identifier
|
|
||||||
}
|
}
|
||||||
)
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class InterventionIdentifierGeneratorView(AbstractIdentifierGeneratorView):
|
||||||
@any_group_check
|
_MODEL = Intervention
|
||||||
@uuid_required
|
|
||||||
def detail_view(request: HttpRequest, id: str):
|
|
||||||
""" Renders a detail view for viewing an intervention's data
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (HttpRequest): The incoming request
|
|
||||||
id (str): The intervention's id
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
template = "intervention/detail/view.html"
|
|
||||||
|
|
||||||
# Fetch data, filter out deleted related data
|
|
||||||
intervention = get_object_or_404(
|
|
||||||
Intervention.objects.select_related(
|
|
||||||
"geometry",
|
|
||||||
"legal",
|
|
||||||
"responsible",
|
|
||||||
).prefetch_related(
|
|
||||||
"legal__revocations",
|
|
||||||
),
|
|
||||||
id=id,
|
|
||||||
deleted=None
|
|
||||||
)
|
|
||||||
compensations = intervention.compensations.filter(
|
|
||||||
deleted=None,
|
|
||||||
)
|
|
||||||
_user = request.user
|
|
||||||
is_data_shared = intervention.is_shared_with(user=_user)
|
|
||||||
|
|
||||||
geom_form = SimpleGeomForm(
|
|
||||||
instance=intervention,
|
|
||||||
)
|
|
||||||
last_checked = intervention.get_last_checked_action()
|
|
||||||
last_checked_tooltip = ""
|
|
||||||
if last_checked:
|
|
||||||
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
|
|
||||||
last_checked.get_timestamp_str_formatted(),
|
|
||||||
last_checked.user
|
|
||||||
)
|
|
||||||
|
|
||||||
has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists()
|
|
||||||
|
|
||||||
requesting_user_is_only_shared_user = intervention.is_only_shared_with(_user)
|
|
||||||
if requesting_user_is_only_shared_user:
|
|
||||||
messages.info(
|
|
||||||
request,
|
|
||||||
DO_NOT_FORGET_TO_SHARE
|
|
||||||
)
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"obj": intervention,
|
|
||||||
"last_checked": last_checked,
|
|
||||||
"last_checked_tooltip": last_checked_tooltip,
|
|
||||||
"compensations": compensations,
|
|
||||||
"has_access": is_data_shared,
|
|
||||||
"geom_form": geom_form,
|
|
||||||
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
|
||||||
"is_zb_member": _user.in_group(ZB_GROUP),
|
|
||||||
"is_ets_member": _user.in_group(ETS_GROUP),
|
|
||||||
"LANIS_LINK": intervention.get_LANIS_link(),
|
|
||||||
"has_payment_without_document": has_payment_without_document,
|
|
||||||
TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}",
|
|
||||||
}
|
|
||||||
|
|
||||||
request = intervention.set_status_messages(request)
|
|
||||||
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class EditInterventionView(LoginRequiredMixin, View):
|
||||||
@default_group_required
|
_TEMPLATE = "intervention/form/view.html"
|
||||||
@shared_access_required(Intervention, "id")
|
|
||||||
def edit_view(request: HttpRequest, id: str):
|
|
||||||
"""
|
|
||||||
Renders a view for editing interventions
|
|
||||||
|
|
||||||
Args:
|
@method_decorator(default_group_required)
|
||||||
request (HttpRequest): The incoming request
|
@method_decorator(shared_access_required(Intervention, "id"))
|
||||||
|
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
"""
|
||||||
|
Renders a view for editing interventions
|
||||||
|
|
||||||
Returns:
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): The intervention identifier
|
||||||
|
|
||||||
"""
|
Returns:
|
||||||
template = "intervention/form/view.html"
|
HttpResponse: The rendered view
|
||||||
# Get object from db
|
"""
|
||||||
intervention = get_object_or_404(Intervention, id=id)
|
|
||||||
if intervention.is_recorded:
|
|
||||||
messages.info(
|
|
||||||
request,
|
|
||||||
RECORDED_BLOCKS_EDIT
|
|
||||||
)
|
|
||||||
return redirect("intervention:detail", id=id)
|
|
||||||
|
|
||||||
# Create forms, initialize with values from db/from POST request
|
# Get object from db
|
||||||
data_form = EditInterventionForm(request.POST or None, instance=intervention)
|
intervention = get_object_or_404(Intervention, id=id)
|
||||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
|
if intervention.is_recorded:
|
||||||
if request.method == "POST":
|
messages.info(
|
||||||
|
request,
|
||||||
|
RECORDED_BLOCKS_EDIT
|
||||||
|
)
|
||||||
|
return redirect("intervention:detail", id=id)
|
||||||
|
|
||||||
|
# Create forms, initialize with values from db/from POST request
|
||||||
|
data_form = EditInterventionForm(request.POST or None, instance=intervention)
|
||||||
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
|
||||||
|
context = {
|
||||||
|
"form": data_form,
|
||||||
|
"geom_form": geom_form,
|
||||||
|
TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier),
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
|
|
||||||
|
@method_decorator(default_group_required)
|
||||||
|
@method_decorator(shared_access_required(Intervention, "id"))
|
||||||
|
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
"""
|
||||||
|
Process saved form content
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): The intervention id
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
HttpResponse:
|
||||||
|
"""
|
||||||
|
# Get object from db
|
||||||
|
intervention = get_object_or_404(Intervention, id=id)
|
||||||
|
if intervention.is_recorded:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
RECORDED_BLOCKS_EDIT
|
||||||
|
)
|
||||||
|
return redirect("intervention:detail", id=id)
|
||||||
|
|
||||||
|
# Create forms, initialize with values from db/from POST request
|
||||||
|
data_form = EditInterventionForm(request.POST or None, instance=intervention)
|
||||||
|
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
|
||||||
if data_form.is_valid() and geom_form.is_valid():
|
if data_form.is_valid() and geom_form.is_valid():
|
||||||
# The data form takes the geom form for processing, as well as the performing user
|
# The data form takes the geom form for processing, as well as the performing user
|
||||||
# Save the current state of recorded|checked to inform the user in case of a status reset due to editing
|
# Save the current state of recorded|checked to inform the user in case of a status reset due to editing
|
||||||
@@ -236,45 +212,22 @@ def edit_view(request: HttpRequest, id: str):
|
|||||||
messages.success(request, _("Intervention {} edited").format(intervention.identifier))
|
messages.success(request, _("Intervention {} edited").format(intervention.identifier))
|
||||||
if intervention_is_checked:
|
if intervention_is_checked:
|
||||||
messages.info(request, CHECK_STATE_RESET)
|
messages.info(request, CHECK_STATE_RESET)
|
||||||
if geom_form.geometry_simplified:
|
if geom_form.has_geometry_simplified():
|
||||||
messages.info(
|
messages.info(
|
||||||
request,
|
request,
|
||||||
GEOMETRY_SIMPLIFIED
|
GEOMETRY_SIMPLIFIED
|
||||||
)
|
)
|
||||||
|
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||||
|
if num_ignored_geometries > 0:
|
||||||
|
messages.info(
|
||||||
|
request,
|
||||||
|
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||||
|
)
|
||||||
return redirect("intervention:detail", id=intervention.id)
|
return redirect("intervention:detail", id=intervention.id)
|
||||||
else:
|
context = {
|
||||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
"form": data_form,
|
||||||
else:
|
"geom_form": geom_form,
|
||||||
# For clarification: nothing in this case
|
TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier),
|
||||||
pass
|
}
|
||||||
context = {
|
context = BaseContext(request, context).context
|
||||||
"form": data_form,
|
return render(request, self._TEMPLATE, context)
|
||||||
"geom_form": geom_form,
|
|
||||||
TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier),
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required_modal
|
|
||||||
@login_required
|
|
||||||
@default_group_required
|
|
||||||
@shared_access_required(Intervention, "id")
|
|
||||||
def remove_view(request: HttpRequest, id: str):
|
|
||||||
""" Renders a remove view for this intervention
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (HttpRequest): The incoming request
|
|
||||||
id (str): The uuid id as string
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
obj = Intervention.objects.get(id=id)
|
|
||||||
identifier = obj.identifier
|
|
||||||
form = RemoveModalForm(request.POST or None, instance=obj, request=request)
|
|
||||||
return form.process_request(
|
|
||||||
request,
|
|
||||||
_("{} removed").format(identifier),
|
|
||||||
redirect_url=reverse("intervention:index")
|
|
||||||
)
|
|
||||||
|
|||||||
20
intervention/views/remove.py
Normal file
20
intervention/views/remove.py
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Created on: 14.12.25
|
||||||
|
|
||||||
|
"""
|
||||||
|
from django.http import HttpRequest, HttpResponse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
|
||||||
|
from intervention.models import Intervention
|
||||||
|
from konova.decorators import shared_access_required
|
||||||
|
from konova.views.remove import AbstractRemoveView
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveInterventionView(AbstractRemoveView):
|
||||||
|
_MODEL = Intervention
|
||||||
|
_REDIRECT_URL = "intervention:index"
|
||||||
|
|
||||||
|
@method_decorator(shared_access_required(Intervention, "id"))
|
||||||
|
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||||
|
return super().get(request, *args, **kwargs)
|
||||||
@@ -5,72 +5,78 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
|||||||
Created on: 19.08.22
|
Created on: 19.08.22
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, render
|
from django.shortcuts import 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 _
|
||||||
|
|
||||||
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.qrcode import QrCode
|
||||||
|
from konova.views.report import AbstractPublicReportView
|
||||||
|
|
||||||
|
|
||||||
@uuid_required
|
class InterventionPublicReportView(AbstractPublicReportView):
|
||||||
def report_view(request: HttpRequest, id: str):
|
_TEMPLATE = "intervention/report/report.html"
|
||||||
""" Renders the public report view
|
|
||||||
|
|
||||||
Args:
|
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
request (HttpRequest): The incoming request
|
""" Renders the public report view
|
||||||
id (str): The id of the intervention
|
|
||||||
|
|
||||||
Returns:
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): The id of the intervention
|
||||||
|
|
||||||
"""
|
Returns:
|
||||||
template = "intervention/report/report.html"
|
|
||||||
intervention = get_object_or_404(Intervention, id=id)
|
"""
|
||||||
|
intervention = get_object_or_404(Intervention, id=id)
|
||||||
|
|
||||||
|
tab_title = _("Report {}").format(intervention.identifier)
|
||||||
|
# If intervention is not recorded (yet or currently) we need to render another template without any data
|
||||||
|
if not intervention.is_ready_for_publish():
|
||||||
|
template = "report/unavailable.html"
|
||||||
|
context = {
|
||||||
|
TAB_TITLE_IDENTIFIER: tab_title,
|
||||||
|
}
|
||||||
|
context = BaseContext(request, context).context
|
||||||
|
return render(request, template, context)
|
||||||
|
|
||||||
|
# Prepare data for map viewer
|
||||||
|
geom_form = SimpleGeomForm(
|
||||||
|
instance=intervention
|
||||||
|
)
|
||||||
|
parcels = intervention.get_underlying_parcels()
|
||||||
|
|
||||||
|
distinct_deductions = intervention.deductions.all().distinct(
|
||||||
|
"account"
|
||||||
|
)
|
||||||
|
|
||||||
|
qrcode = QrCode(
|
||||||
|
content=request.build_absolute_uri(reverse("intervention:report", args=(id,))),
|
||||||
|
size=10
|
||||||
|
)
|
||||||
|
qrcode_lanis = QrCode(
|
||||||
|
content=intervention.get_LANIS_link(),
|
||||||
|
size=7
|
||||||
|
)
|
||||||
|
|
||||||
tab_title = _("Report {}").format(intervention.identifier)
|
|
||||||
# If intervention is not recorded (yet or currently) we need to render another template without any data
|
|
||||||
if not intervention.is_ready_for_publish():
|
|
||||||
template = "report/unavailable.html"
|
|
||||||
context = {
|
context = {
|
||||||
|
"obj": intervention,
|
||||||
|
"deductions": distinct_deductions,
|
||||||
|
"qrcode": {
|
||||||
|
"img": qrcode.get_img(),
|
||||||
|
"url": qrcode.get_content(),
|
||||||
|
},
|
||||||
|
"qrcode_lanis": {
|
||||||
|
"img": qrcode_lanis.get_img(),
|
||||||
|
"url": qrcode_lanis.get_content(),
|
||||||
|
},
|
||||||
|
"geom_form": geom_form,
|
||||||
|
"parcels": parcels,
|
||||||
|
"tables_scrollable": False,
|
||||||
TAB_TITLE_IDENTIFIER: tab_title,
|
TAB_TITLE_IDENTIFIER: tab_title,
|
||||||
}
|
}
|
||||||
context = BaseContext(request, context).context
|
context = BaseContext(request, context).context
|
||||||
return render(request, template, context)
|
return render(request, self._TEMPLATE, context)
|
||||||
|
|
||||||
# Prepare data for map viewer
|
|
||||||
geom_form = SimpleGeomForm(
|
|
||||||
instance=intervention
|
|
||||||
)
|
|
||||||
parcels = intervention.get_underlying_parcels()
|
|
||||||
|
|
||||||
distinct_deductions = intervention.deductions.all().distinct(
|
|
||||||
"account"
|
|
||||||
)
|
|
||||||
qrcode_url = request.build_absolute_uri(reverse("intervention:report", args=(id,)))
|
|
||||||
qrcode_img = generate_qr_code(qrcode_url, 10)
|
|
||||||
qrcode_lanis_url = intervention.get_LANIS_link()
|
|
||||||
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
|
|
||||||
|
|
||||||
context = {
|
|
||||||
"obj": intervention,
|
|
||||||
"deductions": distinct_deductions,
|
|
||||||
"qrcode": {
|
|
||||||
"img": qrcode_img,
|
|
||||||
"url": qrcode_url,
|
|
||||||
},
|
|
||||||
"qrcode_lanis": {
|
|
||||||
"img": qrcode_img_lanis,
|
|
||||||
"url": qrcode_lanis_url,
|
|
||||||
},
|
|
||||||
"geom_form": geom_form,
|
|
||||||
"parcels": parcels,
|
|
||||||
"tables_scrollable": False,
|
|
||||||
TAB_TITLE_IDENTIFIER: tab_title,
|
|
||||||
}
|
|
||||||
context = BaseContext(request, context).context
|
|
||||||
return render(request, template, context)
|
|
||||||
@@ -6,10 +6,12 @@ Created on: 19.08.22
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
from django.contrib import messages
|
from django.contrib import messages
|
||||||
from django.contrib.auth.decorators import login_required
|
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||||
from django.http import HttpRequest
|
from django.http import HttpRequest, HttpResponse
|
||||||
from django.shortcuts import get_object_or_404, redirect
|
from django.shortcuts import get_object_or_404, redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
|
from django.utils.decorators import method_decorator
|
||||||
|
from django.views import View
|
||||||
|
|
||||||
from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \
|
from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \
|
||||||
RemoveRevocationModalForm
|
RemoveRevocationModalForm
|
||||||
@@ -19,100 +21,125 @@ from konova.utils.documents import get_document
|
|||||||
from konova.utils.message_templates import REVOCATION_ADDED, DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED
|
from konova.utils.message_templates import REVOCATION_ADDED, DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class NewInterventionRevocationView(LoginRequiredMixin, View):
|
||||||
@default_group_required
|
def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
@shared_access_required(Intervention, "id")
|
""" Renders sharing form for an intervention
|
||||||
def new_revocation_view(request: HttpRequest, id: str):
|
|
||||||
""" Renders sharing form for an intervention
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request (HttpRequest): The incoming request
|
request (HttpRequest): The incoming request
|
||||||
id (str): Intervention's id
|
id (str): Intervention's id
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
intervention = get_object_or_404(Intervention, id=id)
|
intervention = get_object_or_404(Intervention, id=id)
|
||||||
form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, request=request)
|
form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention,
|
||||||
return form.process_request(
|
request=request)
|
||||||
request,
|
return form.process_request(
|
||||||
msg_success=REVOCATION_ADDED,
|
|
||||||
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
|
||||||
@default_group_required
|
|
||||||
def get_revocation_view(request: HttpRequest, doc_id: str):
|
|
||||||
""" Returns the revocation document as downloadable file
|
|
||||||
|
|
||||||
Wraps the generic document fetcher function from konova.utils.
|
|
||||||
|
|
||||||
Args:
|
|
||||||
request (HttpRequest): The incoming request
|
|
||||||
doc_id (str): The document id
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
doc = get_object_or_404(RevocationDocument, id=doc_id)
|
|
||||||
# File download only possible if related instance is shared with user
|
|
||||||
if not doc.instance.legal.intervention.users.filter(id=request.user.id):
|
|
||||||
messages.info(
|
|
||||||
request,
|
request,
|
||||||
DATA_UNSHARED
|
msg_success=REVOCATION_ADDED,
|
||||||
|
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
|
||||||
)
|
)
|
||||||
return redirect("intervention:detail", id=doc.instance.id)
|
|
||||||
return get_document(doc)
|
@method_decorator(default_group_required)
|
||||||
|
@method_decorator(shared_access_required(Intervention, "id"))
|
||||||
|
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
return self.__process_request(request, id, *args, **kwargs)
|
||||||
|
|
||||||
|
@method_decorator(default_group_required)
|
||||||
|
@method_decorator(shared_access_required(Intervention, "id"))
|
||||||
|
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
return self.__process_request(request, id, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
class GetInterventionRevocationView(LoginRequiredMixin, View):
|
||||||
@default_group_required
|
@method_decorator(default_group_required)
|
||||||
@shared_access_required(Intervention, "id")
|
def get(self, request: HttpRequest, doc_id: str, *args, **kwargs) -> HttpResponse:
|
||||||
def edit_revocation_view(request: HttpRequest, id: str, revocation_id: str):
|
""" Returns the revocation document as downloadable file
|
||||||
""" Renders a edit view for a revocation
|
|
||||||
|
|
||||||
Args:
|
Wraps the generic document fetcher function from konova.utils.
|
||||||
request (HttpRequest): The incoming request
|
|
||||||
id (str): The intervention's id as string
|
|
||||||
revocation_id (str): The revocation's id as string
|
|
||||||
|
|
||||||
Returns:
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
doc_id (str): The document id
|
||||||
|
|
||||||
"""
|
Returns:
|
||||||
intervention = get_object_or_404(Intervention, id=id)
|
|
||||||
revocation = get_object_or_404(Revocation, id=revocation_id)
|
|
||||||
|
|
||||||
form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, revocation=revocation, request=request)
|
"""
|
||||||
return form.process_request(
|
doc = get_object_or_404(RevocationDocument, id=doc_id)
|
||||||
request,
|
# File download only possible if related instance is shared with user
|
||||||
REVOCATION_EDITED,
|
if not doc.instance.legal.intervention.users.filter(id=request.user.id):
|
||||||
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
|
messages.info(
|
||||||
)
|
request,
|
||||||
|
DATA_UNSHARED
|
||||||
|
)
|
||||||
|
return redirect("intervention:detail", id=doc.instance.id)
|
||||||
|
return get_document(doc)
|
||||||
|
|
||||||
|
|
||||||
@login_required_modal
|
class EditInterventionRevocationView(LoginRequiredMixin, View):
|
||||||
@login_required
|
def __process_request(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
|
||||||
@default_group_required
|
""" Renders a edit view for a revocation
|
||||||
@shared_access_required(Intervention, "id")
|
|
||||||
def remove_revocation_view(request: HttpRequest, id: str, revocation_id: str):
|
|
||||||
""" Renders a remove view for a revocation
|
|
||||||
|
|
||||||
Args:
|
Args:
|
||||||
request (HttpRequest): The incoming request
|
request (HttpRequest): The incoming request
|
||||||
id (str): The intervention's id as string
|
id (str): The intervention's id as string
|
||||||
revocation_id (str): The revocation's id as string
|
revocation_id (str): The revocation's id as string
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
intervention = get_object_or_404(Intervention, id=id)
|
intervention = get_object_or_404(Intervention, id=id)
|
||||||
revocation = get_object_or_404(Revocation, id=revocation_id)
|
revocation = get_object_or_404(Revocation, id=revocation_id)
|
||||||
|
|
||||||
form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation, request=request)
|
form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention,
|
||||||
return form.process_request(
|
revocation=revocation, request=request)
|
||||||
request,
|
return form.process_request(
|
||||||
REVOCATION_REMOVED,
|
request,
|
||||||
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
|
REVOCATION_EDITED,
|
||||||
)
|
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
@method_decorator(default_group_required)
|
||||||
|
@method_decorator(shared_access_required(Intervention, "id"))
|
||||||
|
def get(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
return self.__process_request(request, id, revocation_id, *args, **kwargs)
|
||||||
|
|
||||||
|
@method_decorator(default_group_required)
|
||||||
|
@method_decorator(shared_access_required(Intervention, "id"))
|
||||||
|
def post(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
return self.__process_request(request, id, revocation_id, *args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
class RemoveInterventionRevocationView(LoginRequiredMixin, View):
|
||||||
|
def __process_request(self, request, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
""" Renders a remove view for a revocation
|
||||||
|
|
||||||
|
Args:
|
||||||
|
request (HttpRequest): The incoming request
|
||||||
|
id (str): The intervention's id as string
|
||||||
|
revocation_id (str): The revocation's id as string
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
intervention = get_object_or_404(Intervention, id=id)
|
||||||
|
revocation = get_object_or_404(Revocation, id=revocation_id)
|
||||||
|
|
||||||
|
form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation,
|
||||||
|
request=request)
|
||||||
|
return form.process_request(
|
||||||
|
request,
|
||||||
|
REVOCATION_REMOVED,
|
||||||
|
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
|
||||||
|
)
|
||||||
|
|
||||||
|
@method_decorator(default_group_required)
|
||||||
|
@method_decorator(shared_access_required(Intervention, "id"))
|
||||||
|
def get(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
return self.__process_request(request, id, revocation_id, *args, **kwargs)
|
||||||
|
|
||||||
|
@method_decorator(default_group_required)
|
||||||
|
@method_decorator(shared_access_required(Intervention, "id"))
|
||||||
|
def post(self, request, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
|
||||||
|
return self.__process_request(request, id, revocation_id, *args, **kwargs)
|
||||||
@@ -151,7 +151,7 @@ class ResubmissionAdmin(BaseResourceAdmin):
|
|||||||
|
|
||||||
|
|
||||||
# Outcommented for a cleaner admin backend on production
|
# Outcommented for a cleaner admin backend on production
|
||||||
admin.site.register(Geometry, GeometryAdmin)
|
#admin.site.register(Geometry, GeometryAdmin)
|
||||||
#admin.site.register(Parcel, ParcelAdmin)
|
#admin.site.register(Parcel, ParcelAdmin)
|
||||||
#admin.site.register(District, DistrictAdmin)
|
#admin.site.register(District, DistrictAdmin)
|
||||||
#admin.site.register(Municipal, MunicipalAdmin)
|
#admin.site.register(Municipal, MunicipalAdmin)
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import os
|
import os
|
||||||
|
|
||||||
from celery import Celery
|
from celery import Celery
|
||||||
|
from konova.sub_settings.django_settings import env
|
||||||
|
|
||||||
# Set the default Django settings module for the 'celery' program.
|
# Set the default Django settings module for the 'celery' program.
|
||||||
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'konova.settings')
|
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'konova.settings')
|
||||||
@@ -17,7 +18,7 @@ app.config_from_object('django.conf:settings', namespace='CELERY')
|
|||||||
app.autodiscover_tasks()
|
app.autodiscover_tasks()
|
||||||
|
|
||||||
# Declare redis as broker
|
# Declare redis as broker
|
||||||
app.conf.broker_url = 'redis://localhost:6379/0'
|
app.conf.broker_url = f'redis://{env("REDIS_HOST")}:{env.int("REDIS_PORT")}/0'
|
||||||
|
|
||||||
|
|
||||||
@app.task(bind=True)
|
@app.task(bind=True)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ 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.core.exceptions import BadRequest
|
||||||
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 _
|
||||||
@@ -185,7 +185,7 @@ def uuid_required(function):
|
|||||||
try:
|
try:
|
||||||
uuid = UUID(uuid)
|
uuid = UUID(uuid)
|
||||||
except ValueError:
|
except ValueError:
|
||||||
raise Http404(
|
raise BadRequest(
|
||||||
"Invalid UUID"
|
"Invalid UUID"
|
||||||
)
|
)
|
||||||
return function(request, *args, **kwargs)
|
return function(request, *args, **kwargs)
|
||||||
|
|||||||
55
konova/filters/mixins/user_log.py
Normal file
55
konova/filters/mixins/user_log.py
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||||
|
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||||
|
Created on: 19.08.24
|
||||||
|
|
||||||
|
"""
|
||||||
|
import django_filters
|
||||||
|
from django import forms
|
||||||
|
from django.db.models import QuerySet, Q
|
||||||
|
from django.utils.translation import gettext_lazy as _
|
||||||
|
|
||||||
|
|
||||||
|
class UserLoggedTableFilterMixin(django_filters.FilterSet):
|
||||||
|
ul = django_filters.CharFilter(
|
||||||
|
method="filter_user_log",
|
||||||
|
label=_(""),
|
||||||
|
label_suffix=_(""),
|
||||||
|
widget=forms.TextInput(
|
||||||
|
attrs={
|
||||||
|
"placeholder": _("Logged user"),
|
||||||
|
"title": _("Search for entries where this person has been participated according to log history"),
|
||||||
|
"class": "form-control",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
def filter_user_log(self, queryset, name, value) -> QuerySet:
|
||||||
|
""" Filters queryset depending on value of input
|
||||||
|
|
||||||
|
Args:
|
||||||
|
queryset (QuerySet): Incoming (prefiltered) queryset
|
||||||
|
name (str): Name of input field
|
||||||
|
value (str): Value of input field
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
value = value.replace(",", " ")
|
||||||
|
value = value.strip()
|
||||||
|
values = value.split(" ")
|
||||||
|
|
||||||
|
q = Q()
|
||||||
|
for val in values:
|
||||||
|
q &= (
|
||||||
|
Q(log__user__username__icontains=val) |
|
||||||
|
Q(log__user__first_name__icontains=val) |
|
||||||
|
Q(log__user__last_name__icontains=val)
|
||||||
|
)
|
||||||
|
|
||||||
|
queryset = queryset.filter(q)
|
||||||
|
return queryset
|
||||||
@@ -14,6 +14,7 @@ from konova.filters.mixins.office import ConservationOfficeTableFilterMixin, Reg
|
|||||||
from konova.filters.mixins.record import RecordableTableFilterMixin
|
from konova.filters.mixins.record import RecordableTableFilterMixin
|
||||||
from konova.filters.mixins.self_created import SelfCreatedTableFilterMixin
|
from konova.filters.mixins.self_created import SelfCreatedTableFilterMixin
|
||||||
from konova.filters.mixins.share import ShareableTableFilterMixin
|
from konova.filters.mixins.share import ShareableTableFilterMixin
|
||||||
|
from konova.filters.mixins.user_log import UserLoggedTableFilterMixin
|
||||||
|
|
||||||
|
|
||||||
class AbstractTableFilter(django_filters.FilterSet):
|
class AbstractTableFilter(django_filters.FilterSet):
|
||||||
@@ -40,7 +41,8 @@ class SelectionTableFilter(RegistrationOfficeTableFilterMixin,
|
|||||||
|
|
||||||
class QueryTableFilter(KeywordTableFilterMixin,
|
class QueryTableFilter(KeywordTableFilterMixin,
|
||||||
FileNumberTableFilterMixin,
|
FileNumberTableFilterMixin,
|
||||||
GeoReferencedTableFilterMixin):
|
GeoReferencedTableFilterMixin,
|
||||||
|
UserLoggedTableFilterMixin):
|
||||||
""" TableFilter holding different filter options for query related filtering
|
""" TableFilter holding different filter options for query related filtering
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -25,15 +25,17 @@ class SimpleGeomForm(BaseForm):
|
|||||||
""" A geometry form for rendering geometry read-only using a widget
|
""" A geometry form for rendering geometry read-only using a widget
|
||||||
|
|
||||||
"""
|
"""
|
||||||
read_only = True
|
read_only: bool = True
|
||||||
geometry_simplified = False
|
_geometry_simplified: bool = False
|
||||||
geom = JSONField(
|
output = JSONField(
|
||||||
label=_("Geometry"),
|
label=_("Geometry"),
|
||||||
help_text=_(""),
|
help_text=_(""),
|
||||||
label_suffix="",
|
label_suffix="",
|
||||||
required=False,
|
required=False,
|
||||||
disabled=False,
|
disabled=False,
|
||||||
)
|
)
|
||||||
|
_num_geometries_ignored: int = 0
|
||||||
|
empty = False
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.read_only = kwargs.pop("read_only", True)
|
self.read_only = kwargs.pop("read_only", True)
|
||||||
@@ -48,33 +50,33 @@ class SimpleGeomForm(BaseForm):
|
|||||||
raise AttributeError
|
raise AttributeError
|
||||||
|
|
||||||
geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
|
geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
|
||||||
|
geojson = self._set_geojson_properties(geojson, title=self.instance.identifier or None)
|
||||||
geom = json.dumps(geojson)
|
geom = json.dumps(geojson)
|
||||||
except AttributeError:
|
except AttributeError:
|
||||||
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
|
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
|
||||||
geom = ""
|
geom = json.dumps({})
|
||||||
self.empty = True
|
self.empty = True
|
||||||
|
|
||||||
self.initialize_form_field("geom", geom)
|
self.initialize_form_field("output", geom)
|
||||||
|
|
||||||
def is_valid(self):
|
def is_valid(self):
|
||||||
super().is_valid()
|
super().is_valid()
|
||||||
is_valid = True
|
is_valid = True
|
||||||
|
|
||||||
# Get geojson from form
|
# Make sure invalid geometry is properly rendered again to the user
|
||||||
geom = self.data["geom"]
|
# Therefore: write submitted data back into form field
|
||||||
if geom is None or len(geom) == 0:
|
# (does not matter whether we know if it is valid or invalid)
|
||||||
# empty geometry is a valid geometry
|
submitted_data = self.data["output"]
|
||||||
self.cleaned_data["geom"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt
|
submitted_data = json.loads(submitted_data)
|
||||||
return is_valid
|
submitted_data = self._set_geojson_properties(submitted_data)
|
||||||
|
self.initialize_form_field("output", json.dumps(submitted_data))
|
||||||
|
|
||||||
|
# Get geojson from form for validity checking
|
||||||
|
geom = self.data.get("output", json.dumps({}))
|
||||||
geom = json.loads(geom)
|
geom = json.loads(geom)
|
||||||
|
|
||||||
# Write submitted data back into form field to make sure invalid geometry
|
# Initialize features list with empty MultiPolygon, so that an empty input will result in a
|
||||||
# will be rendered again on failed submit
|
# proper empty MultiPolygon object
|
||||||
self.initialize_form_field("geom", self.data["geom"])
|
|
||||||
|
|
||||||
# Read geojson into gdal geometry
|
|
||||||
# HINT: This can be simplified if the geojson format holds data in epsg:4326 (GDAL provides direct creation for
|
|
||||||
# this case)
|
|
||||||
features = []
|
features = []
|
||||||
features_json = geom.get("features", [])
|
features_json = geom.get("features", [])
|
||||||
accepted_ogr_types = [
|
accepted_ogr_types = [
|
||||||
@@ -83,47 +85,64 @@ class SimpleGeomForm(BaseForm):
|
|||||||
"MultiPolygon",
|
"MultiPolygon",
|
||||||
"MultiPolygon25D",
|
"MultiPolygon25D",
|
||||||
]
|
]
|
||||||
|
# Check validity for each feature of the geometry
|
||||||
for feature in features_json:
|
for feature in features_json:
|
||||||
feature_geom = feature.get("geometry", feature)
|
feature_geom = feature.get("geometry", feature)
|
||||||
if feature_geom is None:
|
if feature_geom is None:
|
||||||
# Fallback for rare cases where a feature does not contain any geometry
|
# Fallback for rare cases where a feature does not contain any geometry
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# Try to create a geometry object from the single feature
|
||||||
feature_geom = json.dumps(feature_geom)
|
feature_geom = json.dumps(feature_geom)
|
||||||
g = gdal.OGRGeometry(feature_geom, srs=DEFAULT_SRID_RLP)
|
g = gdal.OGRGeometry(feature_geom, srs=DEFAULT_SRID_RLP)
|
||||||
|
|
||||||
flatten_geometry = g.coord_dim > 2
|
geometry_has_unwanted_dimensions = g.coord_dim > 2
|
||||||
if flatten_geometry:
|
if geometry_has_unwanted_dimensions:
|
||||||
g = self.__flatten_geom_to_2D(g)
|
g = self.__flatten_geom_to_2D(g)
|
||||||
|
|
||||||
if g.geom_type not in accepted_ogr_types:
|
geometry_type_is_accepted = g.geom_type not in accepted_ogr_types
|
||||||
self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
|
if geometry_type_is_accepted:
|
||||||
|
self.add_error("output", _("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)
|
is_area_valid = self.__is_area_valid(g)
|
||||||
|
if not is_area_valid:
|
||||||
|
# Geometries with an invalid size will not be saved to the db
|
||||||
|
# We assume these are malicious snippets which are not supposed to be in the geometry in the first place
|
||||||
|
self._num_geometries_ignored += 1
|
||||||
|
continue
|
||||||
|
|
||||||
polygon = Polygon.from_ewkt(g.ewkt)
|
# Whatever this geometry object is -> try to create a Polygon from it
|
||||||
is_valid &= polygon.valid
|
# The resulting polygon object automatically detects whether a valid polygon has been created or not
|
||||||
if not polygon.valid:
|
g = Polygon.from_ewkt(g.ewkt)
|
||||||
self.add_error("geom", polygon.valid_reason)
|
is_valid &= g.valid
|
||||||
|
if not g.valid:
|
||||||
|
self.add_error("output", g.valid_reason)
|
||||||
return is_valid
|
return is_valid
|
||||||
|
|
||||||
features.append(polygon)
|
# If the resulting polygon is just a single polygon, we add it to the list of properly casted features
|
||||||
|
if isinstance(g, Polygon):
|
||||||
|
features.append(g)
|
||||||
|
elif isinstance(g, MultiPolygon):
|
||||||
|
# The resulting polygon could be of type MultiPolygon (due to multiple surfaces)
|
||||||
|
# If so, we extract all polygons from the MultiPolygon and extend the casted features list
|
||||||
|
features.extend(list(g))
|
||||||
|
|
||||||
# Unionize all geometry features into one new MultiPolygon
|
# Unionize all polygon features into one new MultiPolygon
|
||||||
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
|
if features:
|
||||||
for feature in features:
|
form_geom = MultiPolygon(*features, srid=DEFAULT_SRID_RLP).unary_union
|
||||||
form_geom = form_geom.union(feature)
|
else:
|
||||||
|
# If no features have been processed, this indicates an empty geometry - so we store an empty geometry
|
||||||
|
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
|
||||||
|
|
||||||
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
|
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
|
||||||
if form_geom.geom_type != "MultiPolygon":
|
form_geom = Geometry.cast_to_multipolygon(form_geom)
|
||||||
form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP)
|
|
||||||
|
|
||||||
# Write unioned Multipolygon into cleaned data
|
# Write unionized Multipolygon back into cleaned data
|
||||||
if self.cleaned_data is None:
|
if self.cleaned_data is None:
|
||||||
self.cleaned_data = {}
|
self.cleaned_data = {}
|
||||||
self.cleaned_data["geom"] = form_geom.ewkt
|
self.cleaned_data["output"] = form_geom.ewkt
|
||||||
|
|
||||||
return is_valid
|
return is_valid
|
||||||
|
|
||||||
@@ -133,7 +152,7 @@ class SimpleGeomForm(BaseForm):
|
|||||||
Returns:
|
Returns:
|
||||||
|
|
||||||
"""
|
"""
|
||||||
geom = self.cleaned_data.get("geom")
|
geom = self.cleaned_data.get("output")
|
||||||
g = gdal.OGRGeometry(geom, srs=DEFAULT_SRID_RLP)
|
g = gdal.OGRGeometry(geom, srs=DEFAULT_SRID_RLP)
|
||||||
num_vertices = g.num_coords
|
num_vertices = g.num_coords
|
||||||
|
|
||||||
@@ -146,15 +165,6 @@ class SimpleGeomForm(BaseForm):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
is_area_valid = geom.area > 1 # > 1m² (SRID:25832)
|
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
|
return is_area_valid
|
||||||
|
|
||||||
def __simplify_geometry(self, geom, max_vert: int):
|
def __simplify_geometry(self, geom, max_vert: int):
|
||||||
@@ -192,14 +202,14 @@ class SimpleGeomForm(BaseForm):
|
|||||||
if self.instance is None or self.instance.geometry is None:
|
if self.instance is None or self.instance.geometry is None:
|
||||||
raise LookupError
|
raise LookupError
|
||||||
geometry = self.instance.geometry
|
geometry = self.instance.geometry
|
||||||
geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP))
|
geometry.geom = self.cleaned_data.get("output", MultiPolygon(srid=DEFAULT_SRID_RLP))
|
||||||
geometry.modified = action
|
geometry.modified = action
|
||||||
|
|
||||||
geometry.save()
|
geometry.save()
|
||||||
except LookupError:
|
except LookupError:
|
||||||
# No geometry or linked instance holding a geometry exist --> create a new one!
|
# No geometry or linked instance holding a geometry exist --> create a new one!
|
||||||
geometry = Geometry.objects.create(
|
geometry = Geometry.objects.create(
|
||||||
geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)),
|
geom=self.cleaned_data.get("output", MultiPolygon(srid=DEFAULT_SRID_RLP)),
|
||||||
created=action,
|
created=action,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -207,13 +217,29 @@ class SimpleGeomForm(BaseForm):
|
|||||||
if not is_vertices_num_valid:
|
if not is_vertices_num_valid:
|
||||||
geometry.geom = self.__simplify_geometry(geometry.geom, max_vert=GEOM_MAX_VERTICES)
|
geometry.geom = self.__simplify_geometry(geometry.geom, max_vert=GEOM_MAX_VERTICES)
|
||||||
geometry.save()
|
geometry.save()
|
||||||
self.geometry_simplified = True
|
self._geometry_simplified = True
|
||||||
|
|
||||||
# Start parcel update and geometry conflict checking procedure in a background process
|
# Start parcel update and geometry conflict checking procedure in a background process
|
||||||
celery_update_parcels.delay(geometry.id)
|
celery_update_parcels.delay(geometry.id)
|
||||||
celery_check_for_geometry_conflicts.delay(geometry.id)
|
celery_check_for_geometry_conflicts.delay(geometry.id)
|
||||||
return geometry
|
return geometry
|
||||||
|
|
||||||
|
def get_num_geometries_ignored(self):
|
||||||
|
""" Returns the number of geometries which had to be ignored for various reasons
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self._num_geometries_ignored
|
||||||
|
|
||||||
|
def has_geometry_simplified(self):
|
||||||
|
""" Returns whether the geometry has been simplified or not.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
|
||||||
|
"""
|
||||||
|
return self._geometry_simplified
|
||||||
|
|
||||||
def __flatten_geom_to_2D(self, geom):
|
def __flatten_geom_to_2D(self, geom):
|
||||||
"""
|
"""
|
||||||
Enforces a given OGRGeometry from higher dimensions into 2D
|
Enforces a given OGRGeometry from higher dimensions into 2D
|
||||||
@@ -223,3 +249,22 @@ class SimpleGeomForm(BaseForm):
|
|||||||
g_wkt = wkt_w.write(geom.geos).decode("utf-8")
|
g_wkt = wkt_w.write(geom.geos).decode("utf-8")
|
||||||
geom = gdal.OGRGeometry(g_wkt)
|
geom = gdal.OGRGeometry(g_wkt)
|
||||||
return geom
|
return geom
|
||||||
|
|
||||||
|
def _set_geojson_properties(self, geojson: dict, title: str = None):
|
||||||
|
""" Toggles the editable property of the geojson for proper handling in map client
|
||||||
|
|
||||||
|
Args:
|
||||||
|
geojson (dict): The GeoJson
|
||||||
|
title (str): An alternative title for the geometry
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
geojson (dict): The altered GeoJson
|
||||||
|
"""
|
||||||
|
features = geojson.get("features", [])
|
||||||
|
for feature in features:
|
||||||
|
if not feature.get("properties", None):
|
||||||
|
feature["properties"] = {}
|
||||||
|
feature["properties"]["editable"] = not self.read_only
|
||||||
|
if title:
|
||||||
|
feature["properties"]["title"] = title
|
||||||
|
return geojson
|
||||||
|
|||||||
@@ -6,11 +6,11 @@ Created on: 26.10.22
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
import zipfile
|
import zipfile
|
||||||
|
from datetime import datetime
|
||||||
from io import BytesIO
|
from io import BytesIO
|
||||||
|
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.utils.datetime_safe import datetime
|
|
||||||
|
|
||||||
from analysis.utils.excel.excel import TempExcelFile
|
from analysis.utils.excel.excel import TempExcelFile
|
||||||
from analysis.utils.report import TimespanReport
|
from analysis.utils.report import TimespanReport
|
||||||
|
|||||||
88
konova/management/commands/recalculate_parcels.py
Normal file
88
konova/management/commands/recalculate_parcels.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||||
|
Contact: michel.peltriaux@sgdnord.rlp.de
|
||||||
|
Created on: 04.01.22
|
||||||
|
|
||||||
|
"""
|
||||||
|
import datetime
|
||||||
|
|
||||||
|
from django.contrib.gis.db.models.functions import Area
|
||||||
|
|
||||||
|
from konova.management.commands.setup import BaseKonovaCommand
|
||||||
|
from konova.models import Geometry, ParcelIntersection
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseKonovaCommand):
|
||||||
|
help = "Recalculates parcels for entries with geometry but missing parcel information"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
parser.add_argument(
|
||||||
|
"--force-all",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="If Attribute set, all entries parcels will be recalculated"
|
||||||
|
)
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
try:
|
||||||
|
self.recalculate_parcels(options)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self._break_line()
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
def recalculate_parcels(self, options: dict):
|
||||||
|
force_all = options.get("force_all", False)
|
||||||
|
|
||||||
|
geometry_objects = Geometry.objects.filter(
|
||||||
|
geom__isempty=False,
|
||||||
|
).exclude(
|
||||||
|
geom=None
|
||||||
|
)
|
||||||
|
|
||||||
|
if not force_all:
|
||||||
|
# Fetch all intersections
|
||||||
|
intersection_objs = ParcelIntersection.objects.filter(
|
||||||
|
geometry__in=geometry_objects
|
||||||
|
)
|
||||||
|
# Just take the geometry ids, which seem to have intersections
|
||||||
|
geom_with_intersection_ids = intersection_objs.values_list(
|
||||||
|
"geometry__id",
|
||||||
|
flat=True
|
||||||
|
)
|
||||||
|
# ... and resolve into Geometry objects again ...
|
||||||
|
intersected_geom_objs = Geometry.objects.filter(
|
||||||
|
id__in=geom_with_intersection_ids
|
||||||
|
)
|
||||||
|
# ... to be able to use the way more efficient difference() function ...
|
||||||
|
geometry_objects_ids = geometry_objects.difference(intersected_geom_objs).values_list("id", flat=True)
|
||||||
|
# ... so we can resolve these into proper Geometry objects again for further annotation usage
|
||||||
|
geometry_objects = Geometry.objects.filter(id__in=geometry_objects_ids)
|
||||||
|
|
||||||
|
self._write_warning("=== Update parcels and districts ===")
|
||||||
|
# Order geometries by size to process smaller once at first
|
||||||
|
geometries = geometry_objects.annotate(
|
||||||
|
area=Area("geom")
|
||||||
|
).order_by(
|
||||||
|
'area'
|
||||||
|
)
|
||||||
|
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
|
||||||
|
i = 0
|
||||||
|
num_geoms = geometries.count()
|
||||||
|
geoms_with_errors = {}
|
||||||
|
for geometry in geometries:
|
||||||
|
self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
|
||||||
|
try:
|
||||||
|
geometry.update_parcels()
|
||||||
|
self._write_warning(f"--- Processed {geometry.get_underlying_parcels().count()} underlying parcels")
|
||||||
|
except Exception as e:
|
||||||
|
geoms_with_errors[geometry.id] = str(e)
|
||||||
|
i += 1
|
||||||
|
self._write_warning(f"--- {i}/{num_geoms} processed")
|
||||||
|
|
||||||
|
self._write_success("Updating parcels done!")
|
||||||
|
|
||||||
|
for key, val in geoms_with_errors.items():
|
||||||
|
self._write_error(f" Error on {key}: {val}")
|
||||||
|
self._write_success(f"{num_geoms - len(geoms_with_errors)} geometries successfuly recalculated!")
|
||||||
|
self._break_line()
|
||||||
54
konova/management/commands/send_to_egon.py
Normal file
54
konova/management/commands/send_to_egon.py
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
"""
|
||||||
|
Author: Michel Peltriaux
|
||||||
|
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
||||||
|
Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||||
|
Created on: 18.06.24
|
||||||
|
|
||||||
|
"""
|
||||||
|
from django.db.models import QuerySet
|
||||||
|
|
||||||
|
from intervention.models import Intervention
|
||||||
|
from konova.management.commands.setup import BaseKonovaCommand
|
||||||
|
|
||||||
|
|
||||||
|
class Command(BaseKonovaCommand):
|
||||||
|
help = "Send specific intervention entries to EGON if there are any payments on them"
|
||||||
|
|
||||||
|
def add_arguments(self, parser):
|
||||||
|
try:
|
||||||
|
parser.add_argument("--intervention-ids", type=str)
|
||||||
|
except ValueError as e:
|
||||||
|
self._write_error(f"Argument error: {e}")
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
def __handle_arguments(self, options):
|
||||||
|
self.intervention_ids = options["intervention_ids"] or ""
|
||||||
|
self.intervention_ids = self.intervention_ids.split(",")
|
||||||
|
self.intervention_ids = [x.strip() for x in self.intervention_ids]
|
||||||
|
|
||||||
|
def handle(self, *args, **options):
|
||||||
|
try:
|
||||||
|
self.__handle_arguments(options)
|
||||||
|
interventions = self.get_interventions()
|
||||||
|
self.process_egon_sending(interventions)
|
||||||
|
except KeyboardInterrupt:
|
||||||
|
self._break_line()
|
||||||
|
exit(-1)
|
||||||
|
|
||||||
|
def get_interventions(self) -> QuerySet:
|
||||||
|
"""
|
||||||
|
Getter for interventions, defined by parameter 'intervention-ids'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
interventions (QuerySet): The interventions
|
||||||
|
"""
|
||||||
|
interventions = Intervention.objects.filter(
|
||||||
|
id__in=self.intervention_ids,
|
||||||
|
)
|
||||||
|
self._write_success(f"... Found {interventions.count()} interventions")
|
||||||
|
return interventions
|
||||||
|
|
||||||
|
def process_egon_sending(self, interventions: QuerySet):
|
||||||
|
for intervention in interventions:
|
||||||
|
intervention.send_data_to_egon()
|
||||||
|
self._write_warning(f"... {intervention.identifier} has been sent to EGON (if it has payments)")
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Michel Peltriaux
|
|
||||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
|
||||||
Contact: michel.peltriaux@sgdnord.rlp.de
|
|
||||||
Created on: 19.08.21
|
|
||||||
|
|
||||||
"""
|
|
||||||
from django.core.management import BaseCommand
|
|
||||||
|
|
||||||
from intervention.models import Intervention
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseCommand):
|
|
||||||
help = "Performs test on collisions using the identifier generation"
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
identifiers = {}
|
|
||||||
max_iterations = 100000
|
|
||||||
try:
|
|
||||||
collisions = 0
|
|
||||||
len_ids = len(identifiers)
|
|
||||||
while len_ids < max_iterations:
|
|
||||||
tmp_intervention = Intervention()
|
|
||||||
_id = tmp_intervention.generate_new_identifier()
|
|
||||||
len_ids = len(identifiers)
|
|
||||||
if _id not in identifiers:
|
|
||||||
if len_ids % (max_iterations/5) == 0:
|
|
||||||
print(len_ids)
|
|
||||||
identifiers[_id] = None
|
|
||||||
else:
|
|
||||||
collisions += 1
|
|
||||||
print("+++ Collision after {} identifiers +++".format(len_ids))
|
|
||||||
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
self._break_line()
|
|
||||||
exit(-1)
|
|
||||||
print(
|
|
||||||
"\n{} collisions in {} identifiers; Collision rate {}%".format(
|
|
||||||
collisions,
|
|
||||||
len_ids,
|
|
||||||
(collisions / len_ids)*100,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
def _break_line(self):
|
|
||||||
""" Simply prints a line break
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
|
|
||||||
"""
|
|
||||||
self.stdout.write("\n")
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
"""
|
|
||||||
Author: Michel Peltriaux
|
|
||||||
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
|
|
||||||
Contact: michel.peltriaux@sgdnord.rlp.de
|
|
||||||
Created on: 04.01.22
|
|
||||||
|
|
||||||
"""
|
|
||||||
import datetime
|
|
||||||
|
|
||||||
from django.contrib.gis.db.models.functions import Area
|
|
||||||
|
|
||||||
from konova.management.commands.setup import BaseKonovaCommand
|
|
||||||
from konova.models import Geometry, Parcel, District
|
|
||||||
|
|
||||||
|
|
||||||
class Command(BaseKonovaCommand):
|
|
||||||
help = "Checks the database' sanity and removes unused entries"
|
|
||||||
|
|
||||||
def handle(self, *args, **options):
|
|
||||||
try:
|
|
||||||
self.update_all_parcels()
|
|
||||||
except KeyboardInterrupt:
|
|
||||||
self._break_line()
|
|
||||||
exit(-1)
|
|
||||||
|
|
||||||
def update_all_parcels(self):
|
|
||||||
num_parcels_before = Parcel.objects.count()
|
|
||||||
num_districts_before = District.objects.count()
|
|
||||||
self._write_warning("=== Update parcels and districts ===")
|
|
||||||
# Order geometries by size to process smaller once at first
|
|
||||||
geometries = Geometry.objects.all().exclude(
|
|
||||||
geom=None
|
|
||||||
).annotate(area=Area("geom")).order_by(
|
|
||||||
'area'
|
|
||||||
)
|
|
||||||
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
|
|
||||||
i = 0
|
|
||||||
num_geoms = geometries.count()
|
|
||||||
for geometry in geometries:
|
|
||||||
self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
|
|
||||||
geometry.update_parcels()
|
|
||||||
self._write_warning(f"--- Processed {geometry.get_underlying_parcels().count()} underlying parcels")
|
|
||||||
i += 1
|
|
||||||
self._write_warning(f"--- {i}/{num_geoms} processed")
|
|
||||||
|
|
||||||
num_parcels_after = Parcel.objects.count()
|
|
||||||
num_districts_after = District.objects.count()
|
|
||||||
if num_parcels_after != num_parcels_before:
|
|
||||||
self._write_error(f"Parcels have changed: {num_parcels_before} to {num_parcels_after} entries. You should run the sanitize command.")
|
|
||||||
if num_districts_after != num_districts_before:
|
|
||||||
self._write_error(f"Districts have changed: {num_districts_before} to {num_districts_after} entries. You should run the sanitize command.")
|
|
||||||
|
|
||||||
self._write_success("Updating parcels done!")
|
|
||||||
self._break_line()
|
|
||||||
@@ -8,9 +8,11 @@ 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
|
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
||||||
from django.db import models, transaction
|
from django.db import models, transaction
|
||||||
|
from django.db.models import Q
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
|
from django.contrib.gis.geos import MultiPolygon
|
||||||
|
|
||||||
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
|
||||||
@@ -108,17 +110,26 @@ class Geometry(BaseResource):
|
|||||||
objs (list): The list of objects
|
objs (list): The list of objects
|
||||||
"""
|
"""
|
||||||
objs = []
|
objs = []
|
||||||
sets = [
|
|
||||||
|
# Some related data sets can be processed rather easily
|
||||||
|
regular_sets = [
|
||||||
self.intervention_set,
|
self.intervention_set,
|
||||||
self.compensation_set,
|
|
||||||
self.ema_set,
|
self.ema_set,
|
||||||
self.ecoaccount_set,
|
self.ecoaccount_set,
|
||||||
]
|
]
|
||||||
for _set in sets:
|
for _set in regular_sets:
|
||||||
set_objs = _set.filter(
|
set_objs = _set.filter(
|
||||||
deleted=None
|
deleted=None
|
||||||
)
|
)
|
||||||
objs += set_objs
|
objs += set_objs
|
||||||
|
|
||||||
|
# ... but we need a special treatment for compensations, since they can be deleted directly OR inherit their
|
||||||
|
# de-facto-deleted status from their deleted parent intervention
|
||||||
|
comp_objs = self.compensation_set.filter(
|
||||||
|
Q(deleted=None) & Q(intervention__deleted=None)
|
||||||
|
)
|
||||||
|
objs += comp_objs
|
||||||
|
|
||||||
return objs
|
return objs
|
||||||
|
|
||||||
def get_data_object(self):
|
def get_data_object(self):
|
||||||
@@ -223,6 +234,17 @@ class Geometry(BaseResource):
|
|||||||
)
|
)
|
||||||
parcel_obj.updated_on = _now
|
parcel_obj.updated_on = _now
|
||||||
parcels_to_update.append(parcel_obj)
|
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:
|
except ObjectDoesNotExist:
|
||||||
# If not existing, create object but do not commit, yet
|
# If not existing, create object but do not commit, yet
|
||||||
parcel_obj = Parcel(
|
parcel_obj = Parcel(
|
||||||
@@ -331,6 +353,7 @@ class Geometry(BaseResource):
|
|||||||
{
|
{
|
||||||
"type": "Feature",
|
"type": "Feature",
|
||||||
"geometry": json.loads(p.json),
|
"geometry": json.loads(p.json),
|
||||||
|
"properties": {},
|
||||||
}
|
}
|
||||||
for p in polygons
|
for p in polygons
|
||||||
]
|
]
|
||||||
@@ -366,13 +389,42 @@ class Geometry(BaseResource):
|
|||||||
diff = geom_envelope - self.geom
|
diff = geom_envelope - self.geom
|
||||||
|
|
||||||
if diff.area == 0:
|
if diff.area == 0:
|
||||||
ratio = 1
|
complexity_factor = 1
|
||||||
else:
|
else:
|
||||||
ratio = self.geom.area / diff.area
|
complexity_factor = self.geom.area / diff.area
|
||||||
|
|
||||||
complexity_factor = 1 - ratio
|
|
||||||
return complexity_factor
|
return complexity_factor
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cast_to_multipolygon(input_geom):
|
||||||
|
""" If input_geom is not a MultiPolygon, cast to MultiPolygon
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_geom ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
output_geom
|
||||||
|
"""
|
||||||
|
output_geom = input_geom
|
||||||
|
if not isinstance(input_geom, MultiPolygon):
|
||||||
|
output_geom = MultiPolygon(input_geom, srid=DEFAULT_SRID_RLP)
|
||||||
|
return output_geom
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def cast_to_rlp_srid(input_geom):
|
||||||
|
""" If input_geom is not of RLP SRID (25832), cast to RLP SRID
|
||||||
|
|
||||||
|
Args:
|
||||||
|
input_geom ():
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
output_geom
|
||||||
|
"""
|
||||||
|
output_geom = input_geom
|
||||||
|
if output_geom.srid != DEFAULT_SRID_RLP:
|
||||||
|
output_geom.transform(DEFAULT_SRID_RLP)
|
||||||
|
return output_geom
|
||||||
|
|
||||||
|
|
||||||
class GeometryConflict(UuidModel):
|
class GeometryConflict(UuidModel):
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -677,12 +677,12 @@ class GeoReferencedMixin(models.Model):
|
|||||||
return request
|
return request
|
||||||
|
|
||||||
instance_objs = []
|
instance_objs = []
|
||||||
conflicts = self.geometry.conflicts_geometries.all()
|
conflicts = self.geometry.conflicts_geometries.iterator()
|
||||||
|
|
||||||
for conflict in conflicts:
|
for conflict in conflicts:
|
||||||
instance_objs += conflict.affected_geometry.get_data_objects()
|
instance_objs += conflict.affected_geometry.get_data_objects()
|
||||||
|
|
||||||
conflicts = self.geometry.conflicted_by_geometries.all()
|
conflicts = self.geometry.conflicted_by_geometries.iterator()
|
||||||
for conflict in conflicts:
|
for conflict in conflicts:
|
||||||
instance_objs += conflict.conflicting_geometry.get_data_objects()
|
instance_objs += conflict.conflicting_geometry.get_data_objects()
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
|||||||
Created on: 16.12.21
|
Created on: 16.12.21
|
||||||
|
|
||||||
"""
|
"""
|
||||||
from django.db import models
|
from django.db import models, transaction
|
||||||
|
|
||||||
from konova.models import UuidModel
|
from konova.models import UuidModel
|
||||||
|
|
||||||
@@ -158,6 +158,46 @@ 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):
|
||||||
"""
|
"""
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user