Compare commits

..

2 Commits

Author SHA1 Message Date
Felix Herold
e14f0700cf Switch to githubb.com for actions loaded
Some checks failed
Gitea Actions Demo / Explore-Gitea-Actions (push) Has been cancelled
2024-04-22 14:04:07 +02:00
Felix Herold
814b2bb15f Add demo.yaml to workflows 2024-04-12 16:17:16 +02:00
164 changed files with 2660 additions and 5799 deletions

View File

@@ -1,48 +0,0 @@
# General
SECRET_KEY=CHANGE_ME
DEBUG=True
ALLOWED_HOSTS=127.0.0.1,localhost,example.org
BASE_URL=http://localhost:8002
ADMINS=Admin1:mail@example.org,Admin2:mail2@example.org
# Database
DB_USER=postgres
DB_PASSWORD=
DB_NAME=konova
DB_HOST=127.0.0.1
DB_PORT=5432
# Redis (for celery)
REDIS_HOST=CHANGE_ME
REDIS_PORT=CHANGE_ME
# E-Mail
SMTP_HOST=localhost
SMTP_PORT=25
REPLY_TO_ADDR=ksp-servicestelle@sgdnord.rlp.de
DEFAULT_FROM_EMAIL=service@ksp.de
# Proxy
PROXY=CHANGE_ME
MAP_PROXY_HOST_WHITELIST=CHANGE_ME_1,CHANGE_ME_2
GEOPORTAL_RLP_USER=CHANGE_ME
GEOPORTAL_RLP_PASSWORD=CHANGE_ME
# Schneider
SCHNEIDER_BASE_URL=https://schneider.naturschutz.rlp.de
SCHNEIDER_AUTH_TOKEN=CHANGE_ME
SCHNEIDER_AUTH_HEADER=auth
# SSO
SSO_SERVER_BASE_URL=https://login.naturschutz.rlp.de
OAUTH_CODE_VERIFIER=CHANGE_ME
OAUTH_CLIENT_ID=CHANGE_ME
OAUTH_CLIENT_SECRET=CHANGE_ME
PROPAGATION_SECRET=CHANGE_ME
# RabbitMQ
## For connections to EGON
EGON_RABBITMQ_HOST=CHANGE_ME
EGON_RABBITMQ_PORT=CHANGE_ME
EGON_RABBITMQ_USER=CHANGE_ME
EGON_RABBITMQ_PW=CHANGE_ME

View File

@@ -0,0 +1,19 @@
name: Gitea Actions Demo
run-name: ${{ gitea.actor }} is testing out Gitea Actions 🚀
on: [push]
jobs:
Explore-Gitea-Actions:
runs-on: ubuntu-latest
steps:
- run: echo "The job was automatically triggered by a ${{ gitea.event_name }} event."
- run: echo "This job is now running on a ${{ runner.os }} server hosted by Gitea!"
- run: echo "The name of your branch is ${{ gitea.ref }} and your repository is ${{ gitea.repository }}."
- name: Check out repository code
uses: https://github.com/actions/checkout@v3
- run: echo "The ${{ gitea.repository }} repository has been cloned to the runner."
- run: echo "The workflow is now ready to test your code on the runner."
- name: List files in the repository
run: |
ls ${{ gitea.workspace }}
- run: echo "This job's status is ${{ job.status }}."

1
.gitignore vendored
View File

@@ -3,4 +3,3 @@
/.idea/
/.coverage
/htmlcov/
/.env

View File

@@ -1,6 +1,6 @@
from django.contrib import admin
from api.models.token import APIUserToken, OAuthToken
from api.models.token import APIUserToken
class APITokenAdmin(admin.ModelAdmin):
@@ -17,17 +17,4 @@ class APITokenAdmin(admin.ModelAdmin):
]
class OAuthTokenAdmin(admin.ModelAdmin):
list_display = [
"access_token",
"refresh_token",
"expires_on",
]
search_fields = [
"access_token",
"refresh_token",
]
admin.site.register(APIUserToken, APITokenAdmin)
admin.site.register(OAuthToken, OAuthTokenAdmin)

View File

@@ -1,26 +0,0 @@
# Generated by Django 5.0.4 on 2024-04-30 07:20
import uuid
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0002_alter_apiusertoken_valid_until'),
]
operations = [
migrations.CreateModel(
name='OAuthToken',
fields=[
('id', models.UUIDField(default=uuid.uuid4, editable=False, primary_key=True, serialize=False)),
('access_token', models.CharField(db_comment='OAuth access token', max_length=255)),
('refresh_token', models.CharField(db_comment='OAuth refresh token', max_length=255)),
('expires_on', models.DateTimeField(db_comment='When the token will be expired')),
],
options={
'abstract': False,
},
),
]

View File

@@ -1,14 +1,7 @@
import json
from datetime import timedelta
import requests
from django.core.exceptions import ObjectDoesNotExist
from django.db import models
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
@@ -51,129 +44,5 @@ class APIUserToken(models.Model):
if token_obj.valid_until is not None and token_obj.valid_until < _today:
raise PermissionError("Token validity expired")
except ObjectDoesNotExist:
raise PermissionError("Token unknown")
raise PermissionError("Credentials invalid")
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

View File

@@ -12,7 +12,7 @@ from django.contrib.gis import geos
from django.urls import reverse
from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase
from konova.models import Geometry
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
class APIV1UpdateTestCase(BaseAPIV1TestCase):
@@ -64,7 +64,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body))
put_geom = Geometry.cast_to_rlp_srid(put_geom)
put_geom.transform(DEFAULT_SRID_RLP)
self.assertEqual(put_geom, self.intervention.geometry.geom)
self.assertEqual(put_props["title"], self.intervention.title)
self.assertNotEqual(modified_on, self.intervention.modified)
@@ -94,7 +94,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body))
put_geom = Geometry.cast_to_rlp_srid(put_geom)
put_geom.transform(DEFAULT_SRID_RLP)
self.assertEqual(put_geom, self.compensation.geometry.geom)
self.assertEqual(put_props["title"], self.compensation.title)
self.assertNotEqual(modified_on, self.compensation.modified)
@@ -124,7 +124,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body))
put_geom = Geometry.cast_to_rlp_srid(put_geom)
put_geom.transform(DEFAULT_SRID_RLP)
self.assertEqual(put_geom, self.eco_account.geometry.geom)
self.assertEqual(put_props["title"], self.eco_account.title)
self.assertNotEqual(modified_on, self.eco_account.modified)
@@ -156,7 +156,7 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body))
put_geom = Geometry.cast_to_rlp_srid(put_geom)
put_geom.transform(DEFAULT_SRID_RLP)
self.assertEqual(put_geom, self.ema.geometry.geom)
self.assertEqual(put_props["title"], self.ema.title)
self.assertNotEqual(modified_on, self.ema.modified)

View File

@@ -11,9 +11,8 @@ from abc import abstractmethod
from django.contrib.gis import geos
from django.contrib.gis.geos import GEOSGeometry
from django.core.paginator import Paginator
from django.db.models import Q
from konova.models import Geometry
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.message_templates import DATA_UNSHARED
@@ -33,8 +32,8 @@ class AbstractModelAPISerializer:
self.lookup = {
"id": None, # must be set
"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)
@abstractmethod
@@ -77,11 +76,7 @@ class AbstractModelAPISerializer:
else:
# Return certain object
self.lookup["id"] = _id
self.shared_lookup = Q(
Q(users__in=[user]) |
Q(teams__in=list(user.shared_teams))
)
self.lookup["users__in"] = [user]
def fetch_and_serialize(self):
""" Serializes the model entry according to the given lookup data
@@ -91,13 +86,7 @@ class AbstractModelAPISerializer:
Returns:
serialized_data (dict)
"""
entries = self.model.objects.filter(
**self.lookup
).filter(
self.shared_lookup
).order_by(
"id"
).distinct()
entries = self.model.objects.filter(**self.lookup).order_by("id")
self.paginator = Paginator(entries, self.rpp)
requested_entries = self.paginator.page(self.page_number)
@@ -145,8 +134,8 @@ class AbstractModelAPISerializer:
if isinstance(geojson, dict):
geojson = json.dumps(geojson)
geometry = geos.fromstr(geojson)
geometry = Geometry.cast_to_rlp_srid(geometry)
geometry = Geometry.cast_to_multipolygon(geometry)
if geometry.srid != DEFAULT_SRID_RLP:
geometry.transform(DEFAULT_SRID_RLP)
return geometry
def _get_obj_from_db(self, id, user):

View File

@@ -6,7 +6,6 @@ Created on: 24.01.22
"""
from django.db import transaction
from django.db.models import Q
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin
from compensation.models import Compensation
@@ -22,10 +21,8 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
def prepare_lookup(self, id, user):
super().prepare_lookup(id, user)
self.shared_lookup = Q(
Q(intervention__users__in=[user]) |
Q(intervention__teams__in=user.shared_teams)
)
del self.lookup["users__in"]
self.lookup["intervention__users__in"] = [user]
def intervention_to_json(self, entry):
return {

View File

@@ -6,7 +6,6 @@ Created on: 28.01.22
"""
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Q
from api.utils.serializer.v1.serializer import DeductableAPISerializerV1Mixin, AbstractModelAPISerializerV1
from compensation.models import EcoAccountDeduction, EcoAccount
@@ -29,11 +28,9 @@ class DeductionAPISerializerV1(AbstractModelAPISerializerV1,
"""
super().prepare_lookup(_id, user)
del self.lookup["users__in"]
del self.lookup["deleted__isnull"]
self.shared_lookup = Q(
Q(intervention__users__in=[user]) |
Q(intervention__teams__in=user.shared_teams)
)
self.lookup["intervention__users__in"] = [user]
def _model_to_geo_json(self, entry):
""" Adds the basic data

View File

@@ -16,8 +16,7 @@ from api.utils.serializer.serializer import AbstractModelAPISerializer
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \
CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_HANDLER_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_HANDLER_ID
from compensation.models import CompensationAction, UnitChoices, CompensationState
from intervention.models import Responsibility, Legal, Handler
from konova.models import Deadline, DeadlineType
@@ -348,7 +347,7 @@ class AbstractCompensationAPISerializerV1Mixin:
try:
biotope_type = entry["biotope"]
biotope_details = [
self._konova_code_from_json(e, CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID) for e in entry["biotope_details"]
self._konova_code_from_json(e, CODELIST_BIOTOPES_EXTRA_CODES_ID) for e in entry["biotope_details"]
]
surface = float(entry["surface"])
except KeyError:

View File

@@ -23,6 +23,11 @@ class AbstractAPIViewV1(AbstractAPIView):
"""
def __init__(self, *args, **kwargs):
self.lookup = {
"id": None, # must be set in subclasses
"deleted__isnull": True,
"users__in": [], # must be set in subclasses
}
super().__init__(*args, **kwargs)
self.serializer = self.serializer()

View File

@@ -50,19 +50,14 @@ class AbstractAPIView(View):
def dispatch(self, request, *args, **kwargs):
try:
# Fetch the proper user from the given request header token
token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
token_user = APIUserToken.get_user_from_token(ksp_token)
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:
if ksp_user != token_user.username:
raise PermissionError(f"Invalid token for {ksp_user}")
self.user = token_user
else:
self.user = token_user
request.user = self.user
if not self.user.is_default_user():

View File

@@ -9,8 +9,7 @@ import collections
from django.core.exceptions import ImproperlyConfigured
from codelist.settings import CODELIST_BIOTOPES_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
from konova.utils.message_templates import UNGROUPED
@@ -85,11 +84,11 @@ class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "short_name"
related_field_name = "long_name"
paginate_by = 200
def __init__(self, *args, **kwargs):
self.c = CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
self.c = CODELIST_BIOTOPES_EXTRA_CODES_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
@@ -104,11 +103,8 @@ class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
qs (QuerySet): The ordered queryset
"""
return qs.order_by(
"short_name",
"long_name",
)
def get_result_label(self, result):
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})"

View File

@@ -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_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_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
CODELIST_COMPENSATION_ACTION_DETAIL_ID
from konova.management.commands.setup import BaseKonovaCommand
from konova.settings import PROXIES
@@ -34,7 +34,6 @@ class Command(BaseKonovaCommand):
CODELIST_REGISTRATION_OFFICE_ID,
CODELIST_BIOTOPES_ID,
CODELIST_BIOTOPES_EXTRA_CODES_ID,
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID,
CODELIST_LAW_ID,
CODELIST_HANDLER_ID,
CODELIST_COMPENSATION_ACTION_ID,
@@ -56,7 +55,7 @@ class Command(BaseKonovaCommand):
content = result.content.decode("utf-8")
root = etree.fromstring(content)
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(
id=list_id,
@@ -75,15 +74,15 @@ class Command(BaseKonovaCommand):
if items is None:
return
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:
children = element.find("items")
_id = element.find("id").text
atom_id = element.find("atomid").text
selectable = element.find("selectable").text.lower()
selectable = bool_map.get(selectable, False)
short_name = element.find("shortname").text or ""
long_name = element.find("longname").text or ""
short_name = element.find("shortname").text
long_name = element.find("longname").text
is_archived = bool_map.get((element.find("archive").text.lower()), False)
code = KonovaCode.objects.get_or_create(

View File

@@ -1,60 +0,0 @@
# Generated by Django 5.0.7 on 2024-08-06 13:40
from django.core.exceptions import ObjectDoesNotExist
from django.db import migrations
from django.db.models import Q
from codelist.settings import CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
def migrate_975_to_288(apps, schema_editor):
KonovaCodeList = apps.get_model('codelist', 'KonovaCodeList')
CompensationState = apps.get_model('compensation', 'CompensationState')
try:
list_288 = KonovaCodeList.objects.get(
id=CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
).codes.all()
except ObjectDoesNotExist:
raise AssertionError("KonovaCodeList 288 does not exist. Did you run 'update_codelist' before migrating?")
states_with_extra_code = CompensationState.objects.filter(
~Q(biotope_type_details=None)
)
print(f"... Found {states_with_extra_code.count()} biotope state entries")
for state in states_with_extra_code:
extra_codes_975 = state.biotope_type_details.filter(
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID]
)
count_extra_codes_975 = extra_codes_975.count()
if count_extra_codes_975 > 0:
print(f" --> Found {count_extra_codes_975} codes from list 975 in biotope entry {state.id}")
extra_codes_288 = []
for extra_code_975 in extra_codes_975:
atom_id = extra_code_975.atom_id
codes_from_288 = list_288.filter(
atom_id=atom_id,
)
extra_codes_288 += codes_from_288
state.biotope_type_details.set(extra_codes_288)
print(" --> Migrated to list 288 for all biotope entries")
class Migration(migrations.Migration):
dependencies = [
('codelist', '0001_initial'),
('compensation', '0003_auto_20220202_0846'),
]
# If migration of codelist is not necessary, this variable can shut down the logic whilst not disturbing the
# migration history
EXECUTE_CODELIST_MIGRATION = True
operations = []
if EXECUTE_CODELIST_MIGRATION:
operations.append(migrations.RunPython(migrate_975_to_288))

View File

@@ -1,25 +0,0 @@
# Generated by Django 5.0.8 on 2024-08-26 16:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('codelist', '0002_migrate_975_to_288'),
]
operations = [
migrations.AlterField(
model_name='konovacode',
name='long_name',
field=models.CharField(blank=True, default="", max_length=1000),
preserve_default=False,
),
migrations.AlterField(
model_name='konovacode',
name='short_name',
field=models.CharField(blank=True, default="", help_text='Short version of long name', max_length=500),
preserve_default=False,
),
]

View File

@@ -25,11 +25,13 @@ class KonovaCode(models.Model):
)
short_name = models.CharField(
max_length=500,
null=True,
blank=True,
help_text="Short version of long name",
)
long_name = models.CharField(
max_length=1000,
null=True,
blank=True,
help_text="",
)
@@ -48,28 +50,12 @@ class KonovaCode(models.Model):
def __str__(self, with_parent: bool = True):
ret_val = ""
long_name = self.long_name
short_name = self.short_name
both_names_exist = long_name is not None and short_name is not None
if both_names_exist:
if with_parent and self.parent:
parent_short_name_exists = self.parent.short_name is not None
parent_long_name_exists = self.parent.long_name is not None
if parent_long_name_exists:
ret_val += self.parent.long_name + " > "
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)
if self.parent and self.parent.long_name and with_parent:
ret_val += self.parent.long_name + " > "
ret_val += self.long_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
ret_val += f" ({self.short_name})"
return ret_val
@property
@@ -89,8 +75,7 @@ class KonovaCode(models.Model):
return self
children = KonovaCode.objects.filter(
parent=self,
is_archived=False,
parent=self
).order_by(
order_by
)

View File

@@ -15,8 +15,7 @@ CODELIST_CONSERVATION_OFFICE_ID = 907 # CLNaturschutzbehörden
CODELIST_REGISTRATION_OFFICE_ID = 1053 # CLZulassungsbehörden
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_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_BIOTOPES_EXTRA_CODES_ID = 975 # CLZusatzbezeichnung
CODELIST_LAW_ID = 1048 # CLVerfahrensrecht
CODELIST_PROCESS_TYPE_ID = 44382 # CLVerfahrenstyp

View File

@@ -45,14 +45,6 @@ class AbstractCompensationAdmin(BaseObjectAdmin):
states = "\n".join(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):
autocomplete_fields = [

View File

@@ -15,7 +15,6 @@ from compensation.models import EcoAccount
from intervention.models import Handler, Responsibility, Legal
from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.settings import ETS_GROUP
from konova.utils import validators
from user.models import User, UserActionLogEntry
@@ -247,13 +246,4 @@ class RemoveEcoAccountModalForm(RemoveModalForm):
"confirm",
_("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

View File

@@ -14,8 +14,7 @@ from django.shortcuts import render
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
from intervention.inputs import CompensationStateTreeRadioSelect
from konova.contexts import BaseContext
from konova.forms.modals import RemoveModalForm, BaseModalForm
@@ -44,7 +43,7 @@ class NewCompensationStateModalForm(BaseModalForm):
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID],
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codelist:biotope-extra-type-autocomplete",

View File

@@ -1,19 +0,0 @@
# Generated by Django 5.0.8 on 2024-08-26 16:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('codelist', '0003_alter_konovacode_long_name_and_more'),
('compensation', '0015_alter_compensation_after_states_and_more'),
]
operations = [
migrations.AlterField(
model_name='compensationstate',
name='biotope_type_details',
field=models.ManyToManyField(blank=True, limit_choices_to={'code_lists__in': [288], 'is_archived': False, 'is_selectable': True}, related_name='+', to='codelist.konovacode'),
),
]

View File

@@ -6,10 +6,10 @@ Created on: 16.11.21
"""
from django.db import models
from django.db.models import Q
from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
from compensation.managers import CompensationStateManager
from konova.models import UuidModel
@@ -34,7 +34,7 @@ class CompensationState(UuidModel):
KonovaCode,
blank=True,
limit_choices_to={
"code_lists__in": [CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID],
"code_lists__in": [CODELIST_BIOTOPES_EXTRA_CODES_ID],
"is_selectable": True,
"is_archived": False,
},

View File

@@ -11,7 +11,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-action' obj.id %}" title="{% trans 'Add new action' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'seedling' %}
@@ -34,7 +34,7 @@
<th scope="col">
{% trans 'Comment' %}
</th>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
@@ -52,7 +52,7 @@
<hr>
{% endfor %}
{% for detail in action.action_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
{% endfor %}
@@ -64,7 +64,7 @@
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -11,7 +11,7 @@
{% fa5_icon 'file-alt' %}
</button>
</a>
{% if is_entry_shared %}
{% if has_access %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:resubmission-create' obj.id %}">
{% fa5_icon 'bell' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'calendar-check' %}
@@ -38,7 +38,7 @@
<th scope="col">
{% trans 'Comment' %}
</th>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
@@ -60,7 +60,7 @@
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'file' %}
@@ -33,7 +33,7 @@
<th scope="col">
{% trans 'Comment' %}
</th>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
@@ -59,7 +59,7 @@
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %}
@@ -35,7 +35,7 @@
<th scope="col">
{% trans 'Surface' %}
</th>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% 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>
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %}
</td>
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %}
@@ -35,7 +35,7 @@
<th scope="col">
{% trans 'Surface' %}
</th>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% 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>
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %}
</td>
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -123,7 +123,7 @@
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
<hr>
{% if is_entry_shared %}
{% if has_access %}
{% for user in obj.intervention.shared_users %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-action' obj.id %}" title="{% trans 'Add new action' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'seedling' %}
@@ -33,7 +33,7 @@
<th scope="col">
{% trans 'Comment' %}
</th>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
@@ -51,7 +51,7 @@
<hr>
{% endfor %}
{% for detail in action.action_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
{% endfor %}
@@ -63,7 +63,7 @@
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -11,7 +11,7 @@
{% fa5_icon 'file-alt' %}
</button>
</a>
{% if is_entry_shared %}
{% if has_access %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'compensation:acc:resubmission-create' obj.id %}">
{% fa5_icon 'bell' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'calendar-check' %}
@@ -58,7 +58,7 @@
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -53,7 +53,7 @@
</td>
<td class="align-middle">
{% if deduction.intervention.recorded %}
<em title="{% trans 'Recorded on' %} {{deduction.intervention.recorded.timestamp}} {% trans 'by' %} {{deduction.intervention.recorded.user}}" class='fas fa-bookmark registered-bookmark'></em>
<em title="{% trans 'Recorded on' %} {{obj.recorded.timestamp}} {% trans 'by' %} {{obj.recorded.user}}" class='fas fa-bookmark registered-bookmark'></em>
{% else %}
<em title="{% trans 'Not recorded yet' %}" class='far fa-bookmark'></em>
{% endif %}
@@ -61,7 +61,7 @@
<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 float-right">
{% if is_default_member and is_entry_shared or is_default_member and user in deduction.intervention.shared_users %}
{% if is_default_member and has_access or is_default_member and user in deduction.intervention.shared_users %}
<button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'file' %}
@@ -57,7 +57,7 @@
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %}
@@ -35,7 +35,7 @@
<th scope="col">
{% trans 'Surface' %}
</th>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% 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>
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %}
</td>
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:acc:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %}
@@ -35,7 +35,7 @@
<th scope="col">
{% trans 'Surface' %}
</th>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% 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>
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %}
</td>
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:acc:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -101,7 +101,7 @@
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
<hr>
{% if is_entry_shared %}
{% if has_access %}
{% for user in obj.users.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}

View File

@@ -54,7 +54,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
post_data = {
"identifier": test_id,
"title": test_title,
"output": geom_json,
"geom": geom_json,
"intervention": self.intervention.id,
}
pre_creation_intervention_log_count = self.intervention.log.count()
@@ -94,7 +94,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
post_data = {
"identifier": test_id,
"title": test_title,
"output": geom_json,
"geom": geom_json,
}
pre_creation_intervention_log_count = self.intervention.log.count()
@@ -150,7 +150,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
"title": new_title,
"intervention": self.intervention.id, # just keep the intervention as it is
"comment": new_comment,
"output": geojson,
"geom": geojson,
}
self.client_user.post(url, post_data)
self.compensation.refresh_from_db()

View File

@@ -46,7 +46,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
post_data = {
"identifier": test_id,
"title": test_title,
"output": geom_json,
"geom": geom_json,
"surface": test_deductable_surface,
"conservation_office": test_conservation_office.id
}
@@ -86,7 +86,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string()
new_geometry = self.create_dummy_geometry()
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
test_conservation_office = self.get_conservation_office_code()
test_deductable_surface = self.eco_account.deductable_surface + 100
@@ -103,7 +103,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
"identifier": new_identifier,
"title": new_title,
"comment": new_comment,
"output": self.create_geojson(new_geometry),
"geom": new_geometry.geojson,
"surface": test_deductable_surface,
"conservation_office": test_conservation_office.id
}

View File

@@ -7,32 +7,30 @@ Created on: 24.08.21
"""
from django.urls import path
from compensation.views.compensation.detail import DetailCompensationView
from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \
GetCompensationDocumentView, RemoveCompensationDocumentView
from compensation.views.compensation.remove import RemoveCompensationView
from compensation.views.compensation.resubmission import CompensationResubmissionView
from compensation.views.compensation.report import CompensationPublicReportView
from compensation.views.compensation.report import report_view
from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \
RemoveCompensationDeadlineView
from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \
RemoveCompensationActionView
from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \
RemoveCompensationStateView
from compensation.views.compensation.compensation import new_view, edit_view, \
IndexCompensationView, CompensationIdentifierGeneratorView
from compensation.views.compensation.compensation import index_view, new_view, new_id_view, detail_view, edit_view, \
remove_view
from compensation.views.compensation.log import CompensationLogView
urlpatterns = [
# Main compensation
path("", IndexCompensationView.as_view(), name="index"),
path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'),
path("", index_view, name="index"),
path('new/id', new_id_view, name='new-id'),
path('new/<intervention_id>', new_view, name='new'),
path('new', new_view, name='new'),
path('<id>', DetailCompensationView.as_view(), name='detail'),
path('<id>', detail_view, name='detail'),
path('<id>/log', CompensationLogView.as_view(), name='log'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', RemoveCompensationView.as_view(), name='remove'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/state/new', NewCompensationStateView.as_view(), name='new-state'),
path('<id>/state/<state_id>/edit', EditCompensationStateView.as_view(), name='state-edit'),
@@ -45,7 +43,7 @@ urlpatterns = [
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>/remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'),
path('<id>/report', CompensationPublicReportView.as_view(), name='report'),
path('<id>/report', report_view, name='report'),
path('<id>/resub', CompensationResubmissionView.as_view(), name='resubmission-create'),
# Documents

View File

@@ -8,13 +8,11 @@ Created on: 24.08.21
from django.urls import path
from compensation.autocomplete.eco_account import EcoAccountAutocomplete
from compensation.views.eco_account.detail import DetailEcoAccountView
from compensation.views.eco_account.eco_account import new_view, edit_view, \
IndexEcoAccountView, EcoAccountIdentifierGeneratorView
from compensation.views.eco_account.eco_account import index_view, new_view, new_id_view, edit_view, remove_view, \
detail_view
from compensation.views.eco_account.log import EcoAccountLogView
from compensation.views.eco_account.record import EcoAccountRecordView
from compensation.views.eco_account.remove import RemoveEcoAccountView
from compensation.views.eco_account.report import EcoAccountPublicReportView
from compensation.views.eco_account.report import report_view
from compensation.views.eco_account.resubmission import EcoAccountResubmissionView
from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \
RemoveEcoAccountStateView
@@ -30,15 +28,15 @@ from compensation.views.eco_account.deduction import NewEcoAccountDeductionView,
app_name = "acc"
urlpatterns = [
path("", IndexEcoAccountView.as_view(), name="index"),
path("", index_view, name="index"),
path('new/', new_view, name='new'),
path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'),
path('<id>', DetailEcoAccountView.as_view(), name='detail'),
path('new/id', new_id_view, name='new-id'),
path('<id>', detail_view, name='detail'),
path('<id>/log', EcoAccountLogView.as_view(), name='log'),
path('<id>/record', EcoAccountRecordView.as_view(), name='record'),
path('<id>/report', EcoAccountPublicReportView.as_view(), name='report'),
path('<id>/report', report_view, name='report'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', RemoveEcoAccountView.as_view(), name='remove'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'),
path('<id>/state/new', NewEcoAccountStateView.as_view(), name='new-state'),

View File

@@ -8,8 +8,10 @@ Created on: 19.08.22
from django.contrib import messages
from django.contrib.auth.decorators import login_required
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.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.forms.compensation import EditCompensationForm, NewCompensationForm
@@ -17,42 +19,47 @@ from compensation.models import Compensation
from compensation.tables.compensation import CompensationTable
from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm
from konova.forms.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.utils.message_templates import RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, \
IDENTIFIER_REPLACED, COMPENSATION_ADDED_TEMPLATE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.index import AbstractIndexView
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \
COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
class IndexCompensationView(AbstractIndexView):
def get(self, request, *args, **kwargs) -> HttpResponse:
"""
Renders the index view for compensation
@login_required
@any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for compensation
Args:
request (HttpRequest): The incoming request
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
compensations = Compensation.objects.filter(
deleted=None, # only show those which are not deleted individually
intervention__deleted=None, # and don't show the ones whose intervention has been deleted
).order_by(
"-modified__timestamp"
)
table = CompensationTable(
request=request,
queryset=compensations
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
Returns:
A rendered view
"""
compensations = Compensation.objects.filter(
deleted=None, # only show those which are not deleted individually
intervention__deleted=None, # and don't show the ones whose intervention has been deleted
).order_by(
"-modified__timestamp"
)
table = CompensationTable(
request=request,
queryset=compensations
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@login_required
@default_group_required
@@ -96,19 +103,11 @@ def new_view(request: HttpRequest, intervention_id: str = None):
)
)
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
if geom_form.has_geometry_simplified():
if geom_form.geometry_simplified:
messages.info(
request,
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)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -124,8 +123,23 @@ def new_view(request: HttpRequest, intervention_id: str = None):
return render(request, template, context)
class CompensationIdentifierGeneratorView(AbstractIdentifierGeneratorView):
_MODEL = Compensation
@login_required
@default_group_required
def new_id_view(request: HttpRequest):
""" 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
}
)
@login_required
@@ -165,19 +179,11 @@ def edit_view(request: HttpRequest, id: str):
if intervention_is_checked:
messages.info(request, CHECK_STATE_RESET)
messages.success(request, _("Compensation {} edited").format(comp.identifier))
if geom_form.has_geometry_simplified():
if geom_form.geometry_simplified:
messages.info(
request,
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)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -191,3 +197,105 @@ def edit_view(request: HttpRequest, id: str):
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@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"),
)

View File

@@ -1,97 +0,0 @@
"""
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)

View File

@@ -1,20 +0,0 @@
"""
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)

View File

@@ -5,7 +5,7 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.http import HttpRequest, HttpResponse
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -14,72 +14,67 @@ from compensation.models import Compensation
from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.qrcode import QrCode
from konova.views.report import AbstractPublicReportView
from konova.utils.generators import generate_qr_code
class CompensationPublicReportView(AbstractPublicReportView):
_TEMPLATE = "compensation/report/compensation/report.html"
def report_view(request: HttpRequest, id: str):
""" Renders the public report view
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" Renders the public report view
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns:
Returns:
"""
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")
"""
# Reuse the compensation report template since compensations are structurally identical
template = "compensation/report/compensation/report.html"
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 = {
"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,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
return render(request, 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)

View File

@@ -1,97 +0,0 @@
"""
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)

View File

@@ -7,49 +7,53 @@ Created on: 19.08.22
"""
from django.contrib import messages
from django.contrib.auth.decorators import login_required
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, redirect, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm
from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm, RemoveEcoAccountModalForm
from compensation.models import EcoAccount
from compensation.tables.eco_account import EcoAccountTable
from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, FORM_INVALID, \
IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.index import AbstractIndexView
from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
class IndexEcoAccountView(AbstractIndexView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""
Renders the index view for eco accounts
@login_required
@any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for eco accounts
Args:
request (HttpRequest): The incoming request
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
eco_accounts = EcoAccount.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
table = EcoAccountTable(
request=request,
queryset=eco_accounts
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
Returns:
A rendered view
"""
template = "generic_index.html"
eco_accounts = EcoAccount.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
table = EcoAccountTable(
request=request,
queryset=eco_accounts
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@@ -80,19 +84,11 @@ def new_view(request: HttpRequest):
)
)
messages.success(request, _("Eco-Account {} added").format(acc.identifier))
if geom_form.has_geometry_simplified():
if geom_form.geometry_simplified:
messages.info(
request,
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)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -107,8 +103,25 @@ def new_view(request: HttpRequest):
context = BaseContext(request, context).context
return render(request, template, context)
class EcoAccountIdentifierGeneratorView(AbstractIdentifierGeneratorView):
_MODEL = EcoAccount
@login_required
@default_group_required
def new_id_view(request: HttpRequest):
""" 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
}
)
@login_required
@default_group_required
@@ -143,19 +156,11 @@ def edit_view(request: HttpRequest, id: str):
# The data form takes the geom form for processing, as well as the performing user
acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
if geom_form.has_geometry_simplified():
if geom_form.geometry_simplified:
messages.info(
request,
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)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -169,3 +174,118 @@ def edit_view(request: HttpRequest, id: str):
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@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"),
)

View File

@@ -1,22 +0,0 @@
"""
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)

View File

@@ -5,7 +5,7 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.http import HttpRequest, HttpResponse
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -14,79 +14,74 @@ from compensation.models import EcoAccount
from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.qrcode import QrCode
from konova.views.report import AbstractPublicReportView
from konova.utils.generators import generate_qr_code
class EcoAccountPublicReportView(AbstractPublicReportView):
_TEMPLATE = "compensation/report/eco_account/report.html"
def report_view(request: HttpRequest, id: str):
""" Renders the public report view
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" Renders the public report view
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns:
Returns:
"""
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)
"""
# 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)
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 = {
"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,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
return render(request, 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)

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-action' obj.id %}" title="{% trans 'Add new action' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'seedling' %}
@@ -49,7 +49,7 @@
<hr>
{% endfor %}
{% for detail in action.action_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.parent.long_name }} > {{detail}}">{{ detail.parent.long_name }} > {{detail.long_name}}</span>
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No action type details' %}">{% trans 'No action type details' %}</span>
{% endfor %}
@@ -61,7 +61,7 @@
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:action-edit' obj.id action.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit action' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -11,14 +11,14 @@
{% fa5_icon 'file-alt' %}
</button>
</a>
{% if is_entry_shared %}
{% if has_access %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}">
{% fa5_icon 'bell' %}
</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 %}
<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 %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'ema:record' obj.id %}">
{% fa5_icon 'bookmark' 'far' %}
@@ -28,21 +28,19 @@
{% fa5_icon 'bookmark' %}
</button>
{% 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 %}
{% if is_default_member %}
<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>
{% 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' %}
<a href="{% url 'ema:edit' obj.id %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}">
{% fa5_icon 'edit' %}
</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 %}
</div>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'calendar-check' %}
@@ -58,7 +58,7 @@
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'file' %}
@@ -57,7 +57,7 @@
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
{% fa5_icon 'plus' %}
{% 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>
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %}
</td>
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'ema:new-state' obj.id %}?before=true" title="{% trans 'Add new state before' %}">
{% fa5_icon 'plus' %}
{% 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>
<br>
{% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span>
{% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %}
</td>
<td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'ema:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -87,7 +87,7 @@
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
<hr>
{% if is_entry_shared %}
{% if has_access %}
{% for user in obj.users.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}

View File

@@ -118,7 +118,6 @@ class EmaViewTestCase(CompensationViewTestCase):
self.index_url,
self.detail_url,
self.report_url,
self.log_url,
]
fail_urls = [
self.new_url,
@@ -134,6 +133,7 @@ class EmaViewTestCase(CompensationViewTestCase):
self.action_remove_url,
self.action_new_url,
self.new_doc_url,
self.log_url,
self.remove_url,
]
self.assert_url_fail(client, fail_urls)

View File

@@ -46,7 +46,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
post_data = {
"identifier": test_id,
"title": test_title,
"output": geom_json,
"geom": geom_json,
"conservation_office": test_conservation_office.id
}
self.client_user.post(new_url, post_data)
@@ -84,7 +84,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string()
new_geometry = self.create_dummy_geometry() # Create an empty geometry
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
test_conservation_office = self.get_conservation_office_code()
check_on_elements = {
@@ -99,7 +99,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
"identifier": new_identifier,
"title": new_title,
"comment": new_comment,
"output": self.create_geojson(new_geometry),
"geom": new_geometry.geojson,
"conservation_office": test_conservation_office.id
}
self.client_user.post(url, post_data)

View File

@@ -48,7 +48,7 @@ class NewEmaFormTestCase(BaseTestCase):
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"output": json.dumps(geom_form_data)
"geom": json.dumps(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 = {
"output": json.dumps(geom_form_data)
"geom": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)

View File

@@ -9,28 +9,26 @@ from django.urls import path
from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView
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.ema import IndexEmaView, EmaIdentifierGeneratorView, EditEmaView, NewEmaView
from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_view, remove_view
from ema.views.log import EmaLogView
from ema.views.record import EmaRecordView
from ema.views.remove import RemoveEmaView
from ema.views.report import EmaPublicReportView
from ema.views.report import report_view
from ema.views.resubmission import EmaResubmissionView
from ema.views.share import EmaShareFormView, EmaShareByTokenView
from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView
app_name = "ema"
urlpatterns = [
path("", IndexEmaView.as_view(), name="index"),
path("new/", NewEmaView.as_view(), name="new"),
path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"),
path("<id>", DetailEmaView.as_view(), name="detail"),
path("", index_view, name="index"),
path("new/", new_view, name="new"),
path("new/id", new_id_view, name="new-id"),
path("<id>", detail_view, name="detail"),
path('<id>/log', EmaLogView.as_view(), name='log'),
path('<id>/edit', EditEmaView.as_view(), name='edit'),
path('<id>/remove', RemoveEmaView.as_view(), name='remove'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/record', EmaRecordView.as_view(), name='record'),
path('<id>/report', EmaPublicReportView.as_view(), name='report'),
path('<id>/report', report_view, name='report'),
path('<id>/resub', EmaResubmissionView.as_view(), name='resubmission-create'),
path('<id>/state/new', NewEmaStateView.as_view(), name='new-state'),

View File

@@ -1,76 +0,0 @@
"""
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)

View File

@@ -7,96 +7,71 @@ Created on: 19.08.22
"""
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, HttpResponse
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views.generic.base import View
from ema.forms import NewEmaForm, EditEmaForm
from ema.models import Ema
from ema.tables import EmaTable
from konova.contexts import BaseContext
from konova.decorators import shared_access_required, conservation_office_group_required
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal, \
uuid_required
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.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.index import AbstractIndexView
DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
class IndexEmaView(AbstractIndexView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
""" Renders the index view for EMAs
@login_required
def index_view(request: HttpRequest):
""" Renders the index view for EMAs
Args:
request (HttpRequest): The incoming request
Args:
request (HttpRequest): The incoming request
Returns:
Returns:
"""
emas = Ema.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
"""
template = "generic_index.html"
emas = Ema.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
table = EmaTable(
request,
queryset=emas
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("EMAs - Overview"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
table = EmaTable(
request,
queryset=emas
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("EMAs - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
class NewEmaView(LoginRequiredMixin, View):
_TEMPLATE = "ema/form/view.html"
@method_decorator(conservation_office_group_required)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
""" GET endpoint
@login_required
@conservation_office_group_required
def new_view(request: HttpRequest):
"""
Renders a view for a new eco account creation
Renders form for new EMA
Args:
request (HttpRequest): The incoming request
Args:
request (HttpRequest): The incoming request
*args ():
**kwargs ():
Returns:
Returns:
"""
data_form = NewEmaForm(request.POST or None)
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)
"""
template = "ema/form/view.html"
data_form = NewEmaForm(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():
generated_identifier = data_form.cleaned_data.get("identifier", None)
ema = data_form.save(request.user, geom_form)
@@ -109,124 +84,176 @@ class NewEmaView(LoginRequiredMixin, View):
)
)
messages.success(request, _("EMA {} added").format(ema.identifier))
if geom_form.has_geometry_simplified():
if geom_form.geometry_simplified:
messages.info(
request,
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)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New EMA"),
else:
# For clarification: nothing in this case
pass
context = {
"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
@method_decorator(conservation_office_group_required)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
return super().get(request, *args, **kwargs)
@login_required
@uuid_required
def detail_view(request: HttpRequest, id: str):
""" Renders the detail view of an EMA
class EditEmaView(LoginRequiredMixin, View):
_TEMPLATE = "compensation/form/view.html"
Args:
request (HttpRequest): The incoming request
id (str): The EMA id
@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
Returns:
Renders form
"""
template = "ema/detail/view.html"
ema = get_object_or_404(Ema, id=id, deleted=None)
Args:
request (HttpRequest): The incoming request
id (str): The ema identifier
*args ():
**kwargs ():
geom_form = SimpleGeomForm(instance=ema)
parcels = ema.get_underlying_parcels()
_user = request.user
is_data_shared = ema.is_shared_with(_user)
Returns:
# Order states according to surface
before_states = ema.before_states.all().order_by("-surface")
after_states = ema.after_states.all().order_by("-surface")
"""
# 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)
# 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)
# 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)
ema.set_status_messages(request)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def post(self, request: HttpRequest, id:str, *args, **kwargs) -> HttpResponse:
""" POST endpoint
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
)
Process submitted forms
context = {
"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 ():
Returns:
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
"""
# 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)
Args:
request (HttpRequest): The incoming request
# 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)
Returns:
"""
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():
# The data form takes the geom form for processing, as well as the performing user
ema = data_form.save(request.user, geom_form)
messages.success(request, _("EMA {} edited").format(ema.identifier))
if geom_form.has_geometry_simplified():
if geom_form.geometry_simplified:
messages.info(
request,
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)
else:
messages.error(request, FORM_INVALID, extra_tags="danger", )
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)
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(ema.identifier),
}
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"),
)

View File

@@ -18,6 +18,7 @@ class EmaLogView(AbstractLogView):
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -1,21 +0,0 @@
"""
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)

View File

@@ -5,7 +5,7 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.http import HttpRequest, HttpResponse
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -14,72 +14,67 @@ from ema.models import Ema
from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.qrcode import QrCode
from konova.views.report import AbstractPublicReportView
from konova.utils.generators import generate_qr_code
class EmaPublicReportView(AbstractPublicReportView):
_TEMPLATE = "ema/report/report.html"
def report_view(request:HttpRequest, id: str):
""" Renders the public report view
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" Renders the public report view
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns:
Returns:
"""
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")
"""
# Reuse the compensation report template since EMAs are structurally identical
template = "ema/report/report.html"
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 = {
"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,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
return render(request, 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)

View File

@@ -37,14 +37,6 @@ class InterventionAdmin(BaseObjectAdmin):
"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):
pass

View File

@@ -5,8 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 30.11.20
"""
from konova.sub_settings.django_settings import env
INTERVENTION_IDENTIFIER_LENGTH = 6
INTERVENTION_IDENTIFIER_TEMPLATE = "EIV-{}"
@@ -16,7 +14,7 @@ INTERVENTION_LANIS_LAYER_NAME_UNRECORDED_OLD_ENTRY = "eiv_unrecorded_old_entries
# EGON connection settings via rabbitmq
# NEEDED FOR BACKWARDS COMPATIBILITY
EGON_RABBITMQ_HOST = env("EGON_RABBITMQ_HOST")
EGON_RABBITMQ_PORT = env("EGON_RABBITMQ_PORT")
EGON_RABBITMQ_USER = env("EGON_RABBITMQ_USER")
EGON_RABBITMQ_PW = env("EGON_RABBITMQ_PW")
EGON_RABBITMQ_HOST = "CHANGE_ME"
EGON_RABBITMQ_PORT = "CHANGE_ME"
EGON_RABBITMQ_USER = "CHANGE_ME"
EGON_RABBITMQ_PW = "CHANGE_ME"

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<a href="{% url 'compensation:new' obj.id %}" title="{% trans 'Add new compensation' %}">
<button class="btn btn-outline-default">
{% fa5_icon 'plus' %}
@@ -32,7 +32,7 @@
<th scope="col">
{% trans 'Title' %}
</th>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
@@ -51,7 +51,7 @@
</td>
<td class="align-middle">{{ comp.title }}</td>
<td>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'intervention:remove-compensation' obj.id comp.id %}" class="btn btn-default btn-modal float-right" title="{% trans 'Remove compensation' %}">
{% fa5_icon 'trash' %}
</button>

View File

@@ -11,7 +11,7 @@
{% fa5_icon 'file-alt' %}
</button>
</a>
{% if is_entry_shared %}
{% if has_access %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'intervention:resubmission-create' obj.id %}">
{% fa5_icon 'bell' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-deduction' obj.id %}" title="{% trans 'Add new deduction' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'tree' %}
@@ -33,7 +33,7 @@
<th scope="col">
{% trans 'Created' %}
</th>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
@@ -56,7 +56,7 @@
<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 float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'intervention:edit-deduction' obj.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-doc' obj.id %}" title="{% trans 'Add new document' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'file' %}
@@ -38,7 +38,7 @@
<th scope="col">
{% trans 'Comment' %}
</th>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
@@ -66,7 +66,7 @@
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'intervention:edit-doc' obj.id doc.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit document' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -10,7 +10,7 @@
</div>
<div class="col-sm-6">
<div class="d-flex justify-content-end">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:pay:new' obj.id %}" title="{% trans 'Add new payment' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'money-bill-wave' %}
@@ -33,7 +33,7 @@
<th class="w-50" scope="col">
{% trans 'Comment' %}
</th>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
@@ -46,24 +46,16 @@
{% for pay in obj.payments.all %}
<tr>
<td class="align-middle">
{% if is_entry_shared %}
{{ pay.amount|floatformat:2 }} €
{% else %}
***
{% endif %}
{{ pay.amount|floatformat:2 }} €
</td>
<td class="align-middle">{{ pay.due_on|default_if_none:"---" }}</td>
<td class="align-middle">
<div class="scroll-150">
{% if is_entry_shared %}
{{ pay.comment }}
{% else %}
{% trans 'This data is not shared with you' %}
{% endif %}
{{ pay.comment }}
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'compensation:pay:edit' obj.id pay.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit payment' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -13,7 +13,7 @@
{% comment %}
Only show add-button if no revocation exists, yet.
{% endcomment %}
{% if is_default_member and is_entry_shared and not obj.legal.revocation %}
{% if is_default_member and has_access and not obj.legal.revocation %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'intervention:new-revocation' obj.id %}" title="{% trans 'Add revocation' %}">
{% fa5_icon 'plus' %}
{% fa5_icon 'ban' %}
@@ -36,7 +36,7 @@
<th scope="col">
{% trans 'Comment' %}
</th>
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<th class="w-10" scope="col">
<span class="float-right">
{% trans 'Action' %}
@@ -64,7 +64,7 @@
</div>
</td>
<td class="align-middle float-right">
{% if is_default_member and is_entry_shared %}
{% if is_default_member and has_access %}
<button data-form-url="{% url 'intervention:edit-revocation' obj.id rev.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit revocation' %}">
{% fa5_icon 'edit' %}
</button>

View File

@@ -129,7 +129,7 @@
{% include 'user/includes/team_data_modal_button.html' %}
{% endfor %}
<hr>
{% if is_entry_shared %}
{% if has_access %}
{% for user in obj.users.all %}
{% include 'user/includes/contact_modal_button.html' %}
{% endfor %}

View File

@@ -60,7 +60,7 @@ class InterventionWorkflowTestCase(BaseWorkflowTestCase):
post_data = {
"identifier": test_id,
"title": test_title,
"output": geom_json,
"geom": geom_json,
}
response = self.client_user.post(
new_url,

View File

@@ -62,7 +62,7 @@ class NewInterventionFormTestCase(BaseTestCase):
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"output": json.dumps(geom_form_data)
"geom": json.dumps(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 = {
"output": json.dumps(geom_form_data)
"geom": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)
@@ -124,7 +124,7 @@ class EditInterventionFormTestCase(NewInterventionFormTestCase):
self.assertIsNotNone(obj.responsible.handler)
self.assertEqual(obj.title, data["title"])
self.assertEqual(obj.comment, data["comment"])
self.assert_equal_geometries(test_geom, obj.geometry.geom)
self.assertTrue(test_geom.equals_exact(obj.geometry.geom, 0.000001))
self.assertEqual(obj.legal.binding_date, today)
self.assertEqual(obj.legal.registration_date, today)

View File

@@ -14,13 +14,10 @@ from intervention.views.deduction import NewInterventionDeductionView, EditInter
RemoveInterventionDeductionView
from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \
RemoveInterventionDocumentView, EditInterventionDocumentView
from intervention.views.intervention import IndexInterventionView, InterventionIdentifierGeneratorView, \
NewInterventionView, EditInterventionView
from intervention.views.remove import RemoveInterventionView
from intervention.views.detail import DetailInterventionView
from intervention.views.intervention import index_view, new_view, new_id_view, detail_view, edit_view, remove_view
from intervention.views.log import InterventionLogView
from intervention.views.record import InterventionRecordView
from intervention.views.report import InterventionPublicReportView
from intervention.views.report import report_view
from intervention.views.resubmission import InterventionResubmissionView
from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \
get_revocation_view
@@ -28,18 +25,18 @@ from intervention.views.share import InterventionShareFormView, InterventionShar
app_name = "intervention"
urlpatterns = [
path("", IndexInterventionView.as_view(), name="index"),
path('new/', NewInterventionView.as_view(), name='new'),
path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'),
path('<id>', DetailInterventionView.as_view(), name='detail'),
path("", index_view, name="index"),
path('new/', new_view, name='new'),
path('new/id', new_id_view, name='new-id'),
path('<id>', detail_view, name='detail'),
path('<id>/log', InterventionLogView.as_view(), name='log'),
path('<id>/edit', EditInterventionView.as_view(), name='edit'),
path('<id>/remove', RemoveInterventionView.as_view(), name='remove'),
path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', remove_view, name='remove'),
path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'),
path('<id>/share', InterventionShareFormView.as_view(), name='share-form'),
path('<id>/check', check_view, name='check'),
path('<id>/record', InterventionRecordView.as_view(), name='record'),
path('<id>/report', InterventionPublicReportView.as_view(), name='report'),
path('<id>/report', report_view, name='report'),
path('<id>/resub', InterventionResubmissionView.as_view(), name='resubmission-create'),
# Compensations

View File

@@ -1,79 +0,0 @@
"""
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)

View File

@@ -7,98 +7,75 @@ Created on: 19.08.22
"""
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, HttpResponse
from django.http import JsonResponse, HttpRequest
from django.shortcuts import get_object_or_404, render, redirect
from django.utils.decorators import method_decorator
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from django.views import View
from intervention.forms.intervention import EditInterventionForm, NewInterventionForm
from intervention.models import Intervention
from intervention.tables import InterventionTable
from konova.contexts import BaseContext
from konova.decorators import default_group_required, shared_access_required
from konova.decorators import default_group_required, shared_access_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm
from konova.forms.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.utils.message_templates import RECORDED_BLOCKS_EDIT, \
CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, \
GEOMETRIES_IGNORED_TEMPLATE
from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.index import AbstractIndexView
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \
CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
class IndexInterventionView(AbstractIndexView):
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""
Renders the index view for Interventions
@login_required
@any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for Interventions
Args:
request (HttpRequest): The incoming request
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
# Filtering by user access is performed in table filter inside InterventionTableFilter class
interventions = Intervention.objects.filter(
deleted=None, # not deleted
).select_related(
"legal"
).order_by(
"-modified__timestamp"
)
table = InterventionTable(
request=request,
queryset=interventions
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Interventions - Overview"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
Returns:
A rendered view
"""
template = "generic_index.html"
# Filtering by user access is performed in table filter inside InterventionTableFilter class
interventions = Intervention.objects.filter(
deleted=None, # not deleted
).select_related(
"legal"
).order_by(
"-modified__timestamp"
)
table = InterventionTable(
request=request,
queryset=interventions
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Interventions - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
class NewInterventionView(LoginRequiredMixin, View):
_TEMPLATE = "intervention/form/view.html"
@login_required
@default_group_required
def new_view(request: HttpRequest):
"""
Renders a view for a new intervention creation
@method_decorator(default_group_required)
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
Args:
request (HttpRequest): The incoming request
"""
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)
Returns:
"""
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():
generated_identifier = data_form.cleaned_data.get("identifier", None)
intervention = data_form.save(request.user, geom_form)
@@ -110,100 +87,147 @@ class NewInterventionView(LoginRequiredMixin, View):
intervention.identifier
)
)
messages.success(request, _("Intervention {} added").format(intervention.identifier))
if geom_form.has_geometry_simplified():
if geom_form.geometry_simplified:
messages.info(
request,
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)
else:
messages.error(request, FORM_INVALID, extra_tags="danger", )
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New intervention"),
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 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)
)
class InterventionIdentifierGeneratorView(AbstractIdentifierGeneratorView):
_MODEL = Intervention
@login_required
@any_group_check
@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)
class EditInterventionView(LoginRequiredMixin, View):
_TEMPLATE = "intervention/form/view.html"
@login_required
@default_group_required
@shared_access_required(Intervention, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing interventions
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
"""
Renders a view for editing interventions
Args:
request (HttpRequest): The incoming request
Args:
request (HttpRequest): The incoming request
id (str): The intervention identifier
Returns:
Returns:
HttpResponse: The rendered view
"""
"""
template = "intervention/form/view.html"
# 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)
# 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)
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)
# 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 request.method == "POST":
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
# Save the current state of recorded|checked to inform the user in case of a status reset due to editing
@@ -212,22 +236,45 @@ class EditInterventionView(LoginRequiredMixin, View):
messages.success(request, _("Intervention {} edited").format(intervention.identifier))
if intervention_is_checked:
messages.info(request, CHECK_STATE_RESET)
if geom_form.has_geometry_simplified():
if geom_form.geometry_simplified:
messages.info(
request,
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)
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)
else:
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(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")
)

View File

@@ -1,20 +0,0 @@
"""
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)

View File

@@ -5,78 +5,72 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.http import HttpRequest, HttpResponse
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.decorators import uuid_required
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.qrcode import QrCode
from konova.views.report import AbstractPublicReportView
from konova.utils.generators import generate_qr_code
class InterventionPublicReportView(AbstractPublicReportView):
_TEMPLATE = "intervention/report/report.html"
@uuid_required
def report_view(request: HttpRequest, id: str):
""" Renders the public report view
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" Renders the public report view
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Args:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns:
Returns:
"""
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
)
"""
template = "intervention/report/report.html"
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 = {
"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,
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, 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_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)

View File

@@ -1,7 +1,6 @@
import os
from celery import Celery
from konova.sub_settings.django_settings import env
# Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'konova.settings')
@@ -18,7 +17,7 @@ app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks()
# Declare redis as broker
app.conf.broker_url = f'redis://{env("REDIS_HOST")}:{env.int("REDIS_PORT")}/0'
app.conf.broker_url = 'redis://localhost:6379/0'
@app.task(bind=True)

View File

@@ -11,7 +11,7 @@ from uuid import UUID
from bootstrap_modal_forms.mixins import is_ajax
from django.contrib import messages
from django.core.exceptions import BadRequest
from django.http import Http404
from django.shortcuts import redirect, get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -185,7 +185,7 @@ def uuid_required(function):
try:
uuid = UUID(uuid)
except ValueError:
raise BadRequest(
raise Http404(
"Invalid UUID"
)
return function(request, *args, **kwargs)

View File

@@ -1,55 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.24
"""
import django_filters
from django import forms
from django.db.models import QuerySet, Q
from django.utils.translation import gettext_lazy as _
class UserLoggedTableFilterMixin(django_filters.FilterSet):
ul = django_filters.CharFilter(
method="filter_user_log",
label=_(""),
label_suffix=_(""),
widget=forms.TextInput(
attrs={
"placeholder": _("Logged user"),
"title": _("Search for entries where this person has been participated according to log history"),
"class": "form-control",
}
),
)
class Meta:
abstract = True
def filter_user_log(self, queryset, name, value) -> QuerySet:
""" Filters queryset depending on value of input
Args:
queryset (QuerySet): Incoming (prefiltered) queryset
name (str): Name of input field
value (str): Value of input field
Returns:
"""
value = value.replace(",", " ")
value = value.strip()
values = value.split(" ")
q = Q()
for val in values:
q &= (
Q(log__user__username__icontains=val) |
Q(log__user__first_name__icontains=val) |
Q(log__user__last_name__icontains=val)
)
queryset = queryset.filter(q)
return queryset

View File

@@ -14,7 +14,6 @@ from konova.filters.mixins.office import ConservationOfficeTableFilterMixin, Reg
from konova.filters.mixins.record import RecordableTableFilterMixin
from konova.filters.mixins.self_created import SelfCreatedTableFilterMixin
from konova.filters.mixins.share import ShareableTableFilterMixin
from konova.filters.mixins.user_log import UserLoggedTableFilterMixin
class AbstractTableFilter(django_filters.FilterSet):
@@ -41,8 +40,7 @@ class SelectionTableFilter(RegistrationOfficeTableFilterMixin,
class QueryTableFilter(KeywordTableFilterMixin,
FileNumberTableFilterMixin,
GeoReferencedTableFilterMixin,
UserLoggedTableFilterMixin):
GeoReferencedTableFilterMixin):
""" TableFilter holding different filter options for query related filtering
"""

View File

@@ -25,17 +25,15 @@ class SimpleGeomForm(BaseForm):
""" A geometry form for rendering geometry read-only using a widget
"""
read_only: bool = True
_geometry_simplified: bool = False
output = JSONField(
read_only = True
geometry_simplified = False
geom = JSONField(
label=_("Geometry"),
help_text=_(""),
label_suffix="",
required=False,
disabled=False,
)
_num_geometries_ignored: int = 0
empty = False
def __init__(self, *args, **kwargs):
self.read_only = kwargs.pop("read_only", True)
@@ -50,33 +48,33 @@ class SimpleGeomForm(BaseForm):
raise AttributeError
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)
except AttributeError:
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
geom = json.dumps({})
geom = ""
self.empty = True
self.initialize_form_field("output", geom)
self.initialize_form_field("geom", geom)
def is_valid(self):
super().is_valid()
is_valid = True
# Make sure invalid geometry is properly rendered again to the user
# Therefore: write submitted data back into form field
# (does not matter whether we know if it is valid or invalid)
submitted_data = self.data["output"]
submitted_data = json.loads(submitted_data)
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({}))
# Get geojson from form
geom = self.data["geom"]
if geom is None or len(geom) == 0:
# empty geometry is a valid geometry
self.cleaned_data["geom"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt
return is_valid
geom = json.loads(geom)
# Initialize features list with empty MultiPolygon, so that an empty input will result in a
# proper empty MultiPolygon object
# Write submitted data back into form field to make sure invalid geometry
# will be rendered again on failed submit
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_json = geom.get("features", [])
accepted_ogr_types = [
@@ -85,64 +83,47 @@ class SimpleGeomForm(BaseForm):
"MultiPolygon",
"MultiPolygon25D",
]
# Check validity for each feature of the geometry
for feature in features_json:
feature_geom = feature.get("geometry", feature)
if feature_geom is None:
# Fallback for rare cases where a feature does not contain any geometry
continue
# Try to create a geometry object from the single feature
feature_geom = json.dumps(feature_geom)
g = gdal.OGRGeometry(feature_geom, srs=DEFAULT_SRID_RLP)
geometry_has_unwanted_dimensions = g.coord_dim > 2
if geometry_has_unwanted_dimensions:
flatten_geometry = g.coord_dim > 2
if flatten_geometry:
g = self.__flatten_geom_to_2D(g)
geometry_type_is_accepted = g.geom_type not in accepted_ogr_types
if geometry_type_is_accepted:
self.add_error("output", _("Only surfaces allowed. Points or lines must be buffered."))
if g.geom_type not in accepted_ogr_types:
self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
is_valid &= False
return is_valid
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
is_valid &= self.__is_area_valid(g)
# Whatever this geometry object is -> try to create a Polygon from it
# The resulting polygon object automatically detects whether a valid polygon has been created or not
g = Polygon.from_ewkt(g.ewkt)
is_valid &= g.valid
if not g.valid:
self.add_error("output", g.valid_reason)
polygon = Polygon.from_ewkt(g.ewkt)
is_valid &= polygon.valid
if not polygon.valid:
self.add_error("geom", polygon.valid_reason)
return is_valid
# 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))
features.append(polygon)
# Unionize all polygon features into one new MultiPolygon
if features:
form_geom = MultiPolygon(*features, srid=DEFAULT_SRID_RLP).unary_union
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)
# Unionize all geometry features into one new MultiPolygon
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
for feature in features:
form_geom = form_geom.union(feature)
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
form_geom = Geometry.cast_to_multipolygon(form_geom)
if form_geom.geom_type != "MultiPolygon":
form_geom = MultiPolygon(form_geom, srid=DEFAULT_SRID_RLP)
# Write unionized Multipolygon back into cleaned data
# Write unioned Multipolygon into cleaned data
if self.cleaned_data is None:
self.cleaned_data = {}
self.cleaned_data["output"] = form_geom.ewkt
self.cleaned_data["geom"] = form_geom.ewkt
return is_valid
@@ -152,7 +133,7 @@ class SimpleGeomForm(BaseForm):
Returns:
"""
geom = self.cleaned_data.get("output")
geom = self.cleaned_data.get("geom")
g = gdal.OGRGeometry(geom, srs=DEFAULT_SRID_RLP)
num_vertices = g.num_coords
@@ -165,6 +146,15 @@ class SimpleGeomForm(BaseForm):
"""
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 {}").format(
float(geom.area)
)
)
return is_area_valid
def __simplify_geometry(self, geom, max_vert: int):
@@ -202,14 +192,14 @@ class SimpleGeomForm(BaseForm):
if self.instance is None or self.instance.geometry is None:
raise LookupError
geometry = self.instance.geometry
geometry.geom = self.cleaned_data.get("output", MultiPolygon(srid=DEFAULT_SRID_RLP))
geometry.geom = self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP))
geometry.modified = action
geometry.save()
except LookupError:
# No geometry or linked instance holding a geometry exist --> create a new one!
geometry = Geometry.objects.create(
geom=self.cleaned_data.get("output", MultiPolygon(srid=DEFAULT_SRID_RLP)),
geom=self.cleaned_data.get("geom", MultiPolygon(srid=DEFAULT_SRID_RLP)),
created=action,
)
@@ -217,29 +207,13 @@ class SimpleGeomForm(BaseForm):
if not is_vertices_num_valid:
geometry.geom = self.__simplify_geometry(geometry.geom, max_vert=GEOM_MAX_VERTICES)
geometry.save()
self._geometry_simplified = True
self.geometry_simplified = True
# Start parcel update and geometry conflict checking procedure in a background process
celery_update_parcels.delay(geometry.id)
celery_check_for_geometry_conflicts.delay(geometry.id)
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):
"""
Enforces a given OGRGeometry from higher dimensions into 2D
@@ -249,22 +223,3 @@ class SimpleGeomForm(BaseForm):
g_wkt = wkt_w.write(geom.geos).decode("utf-8")
geom = gdal.OGRGeometry(g_wkt)
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

View File

@@ -6,11 +6,11 @@ Created on: 26.10.22
"""
import zipfile
from datetime import datetime
from io import BytesIO
from django.core.mail import EmailMessage
from django.utils import timezone
from django.utils.datetime_safe import datetime
from analysis.utils.excel.excel import TempExcelFile
from analysis.utils.report import TimespanReport

View File

@@ -1,88 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.01.22
"""
import datetime
from django.contrib.gis.db.models.functions import Area
from konova.management.commands.setup import BaseKonovaCommand
from konova.models import Geometry, ParcelIntersection
class Command(BaseKonovaCommand):
help = "Recalculates parcels for entries with geometry but missing parcel information"
def add_arguments(self, parser):
parser.add_argument(
"--force-all",
action="store_true",
default=False,
help="If Attribute set, all entries parcels will be recalculated"
)
def handle(self, *args, **options):
try:
self.recalculate_parcels(options)
except KeyboardInterrupt:
self._break_line()
exit(-1)
def recalculate_parcels(self, options: dict):
force_all = options.get("force_all", False)
geometry_objects = Geometry.objects.filter(
geom__isempty=False,
).exclude(
geom=None
)
if not force_all:
# Fetch all intersections
intersection_objs = ParcelIntersection.objects.filter(
geometry__in=geometry_objects
)
# Just take the geometry ids, which seem to have intersections
geom_with_intersection_ids = intersection_objs.values_list(
"geometry__id",
flat=True
)
# ... and resolve into Geometry objects again ...
intersected_geom_objs = Geometry.objects.filter(
id__in=geom_with_intersection_ids
)
# ... to be able to use the way more efficient difference() function ...
geometry_objects_ids = geometry_objects.difference(intersected_geom_objs).values_list("id", flat=True)
# ... so we can resolve these into proper Geometry objects again for further annotation usage
geometry_objects = Geometry.objects.filter(id__in=geometry_objects_ids)
self._write_warning("=== Update parcels and districts ===")
# Order geometries by size to process smaller once at first
geometries = geometry_objects.annotate(
area=Area("geom")
).order_by(
'area'
)
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
i = 0
num_geoms = geometries.count()
geoms_with_errors = {}
for geometry in geometries:
self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
try:
geometry.update_parcels()
self._write_warning(f"--- Processed {geometry.get_underlying_parcels().count()} underlying parcels")
except Exception as e:
geoms_with_errors[geometry.id] = str(e)
i += 1
self._write_warning(f"--- {i}/{num_geoms} processed")
self._write_success("Updating parcels done!")
for key, val in geoms_with_errors.items():
self._write_error(f" Error on {key}: {val}")
self._write_success(f"{num_geoms - len(geoms_with_errors)} geometries successfuly recalculated!")
self._break_line()

View File

@@ -1,54 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.06.24
"""
from django.db.models import QuerySet
from intervention.models import Intervention
from konova.management.commands.setup import BaseKonovaCommand
class Command(BaseKonovaCommand):
help = "Send specific intervention entries to EGON if there are any payments on them"
def add_arguments(self, parser):
try:
parser.add_argument("--intervention-ids", type=str)
except ValueError as e:
self._write_error(f"Argument error: {e}")
exit(-1)
def __handle_arguments(self, options):
self.intervention_ids = options["intervention_ids"] or ""
self.intervention_ids = self.intervention_ids.split(",")
self.intervention_ids = [x.strip() for x in self.intervention_ids]
def handle(self, *args, **options):
try:
self.__handle_arguments(options)
interventions = self.get_interventions()
self.process_egon_sending(interventions)
except KeyboardInterrupt:
self._break_line()
exit(-1)
def get_interventions(self) -> QuerySet:
"""
Getter for interventions, defined by parameter 'intervention-ids'
Returns:
interventions (QuerySet): The interventions
"""
interventions = Intervention.objects.filter(
id__in=self.intervention_ids,
)
self._write_success(f"... Found {interventions.count()} interventions")
return interventions
def process_egon_sending(self, interventions: QuerySet):
for intervention in interventions:
intervention.send_data_to_egon()
self._write_warning(f"... {intervention.identifier} has been sent to EGON (if it has payments)")

View File

@@ -0,0 +1,51 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 19.08.21
"""
from django.core.management import BaseCommand
from intervention.models import Intervention
class Command(BaseCommand):
help = "Performs test on collisions using the identifier generation"
def handle(self, *args, **options):
identifiers = {}
max_iterations = 100000
try:
collisions = 0
len_ids = len(identifiers)
while len_ids < max_iterations:
tmp_intervention = Intervention()
_id = tmp_intervention.generate_new_identifier()
len_ids = len(identifiers)
if _id not in identifiers:
if len_ids % (max_iterations/5) == 0:
print(len_ids)
identifiers[_id] = None
else:
collisions += 1
print("+++ Collision after {} identifiers +++".format(len_ids))
except KeyboardInterrupt:
self._break_line()
exit(-1)
print(
"\n{} collisions in {} identifiers; Collision rate {}%".format(
collisions,
len_ids,
(collisions / len_ids)*100,
)
)
def _break_line(self):
""" Simply prints a line break
Returns:
"""
self.stdout.write("\n")

View File

@@ -0,0 +1,54 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.01.22
"""
import datetime
from django.contrib.gis.db.models.functions import Area
from konova.management.commands.setup import BaseKonovaCommand
from konova.models import Geometry, Parcel, District
class Command(BaseKonovaCommand):
help = "Checks the database' sanity and removes unused entries"
def handle(self, *args, **options):
try:
self.update_all_parcels()
except KeyboardInterrupt:
self._break_line()
exit(-1)
def update_all_parcels(self):
num_parcels_before = Parcel.objects.count()
num_districts_before = District.objects.count()
self._write_warning("=== Update parcels and districts ===")
# Order geometries by size to process smaller once at first
geometries = Geometry.objects.all().exclude(
geom=None
).annotate(area=Area("geom")).order_by(
'area'
)
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
i = 0
num_geoms = geometries.count()
for geometry in geometries:
self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
geometry.update_parcels()
self._write_warning(f"--- Processed {geometry.get_underlying_parcels().count()} underlying parcels")
i += 1
self._write_warning(f"--- {i}/{num_geoms} processed")
num_parcels_after = Parcel.objects.count()
num_districts_after = District.objects.count()
if num_parcels_after != num_parcels_before:
self._write_error(f"Parcels have changed: {num_parcels_before} to {num_parcels_after} entries. You should run the sanitize command.")
if num_districts_after != num_districts_before:
self._write_error(f"Districts have changed: {num_districts_before} to {num_districts_after} entries. You should run the sanitize command.")
self._write_success("Updating parcels done!")
self._break_line()

View File

@@ -8,11 +8,9 @@ Created on: 15.11.21
import json
from django.contrib.gis.db.models import MultiPolygonField
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.core.exceptions import ObjectDoesNotExist
from django.db import models, transaction
from django.db.models import Q
from django.utils import timezone
from django.contrib.gis.geos import MultiPolygon
from konova.models import BaseResource, UuidModel
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
@@ -110,26 +108,17 @@ class Geometry(BaseResource):
objs (list): The list of objects
"""
objs = []
# Some related data sets can be processed rather easily
regular_sets = [
sets = [
self.intervention_set,
self.compensation_set,
self.ema_set,
self.ecoaccount_set,
]
for _set in regular_sets:
for _set in sets:
set_objs = _set.filter(
deleted=None
)
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
def get_data_object(self):
@@ -234,17 +223,6 @@ class Geometry(BaseResource):
)
parcel_obj.updated_on = _now
parcels_to_update.append(parcel_obj)
except MultipleObjectsReturned:
parcel_obj = Parcel.make_unique(
district=district,
municipal=municipal,
parcel_group=parcel_group,
flr=flr_val,
flrstck_nnr=flrstck_nnr,
flrstck_zhlr=flrstck_zhlr,
)
parcel_obj.updated_on = _now
parcels_to_update.append(parcel_obj)
except ObjectDoesNotExist:
# If not existing, create object but do not commit, yet
parcel_obj = Parcel(
@@ -353,7 +331,6 @@ class Geometry(BaseResource):
{
"type": "Feature",
"geometry": json.loads(p.json),
"properties": {},
}
for p in polygons
]
@@ -389,42 +366,13 @@ class Geometry(BaseResource):
diff = geom_envelope - self.geom
if diff.area == 0:
complexity_factor = 1
ratio = 1
else:
complexity_factor = self.geom.area / diff.area
ratio = self.geom.area / diff.area
complexity_factor = 1 - ratio
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):
"""

View File

@@ -677,12 +677,12 @@ class GeoReferencedMixin(models.Model):
return request
instance_objs = []
conflicts = self.geometry.conflicts_geometries.iterator()
conflicts = self.geometry.conflicts_geometries.all()
for conflict in conflicts:
instance_objs += conflict.affected_geometry.get_data_objects()
conflicts = self.geometry.conflicted_by_geometries.iterator()
conflicts = self.geometry.conflicted_by_geometries.all()
for conflict in conflicts:
instance_objs += conflict.conflicting_geometry.get_data_objects()

View File

@@ -5,7 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.12.21
"""
from django.db import models, transaction
from django.db import models
from konova.models import UuidModel
@@ -158,46 +158,6 @@ class Parcel(UuidModel):
def __str__(self):
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):
"""

View File

@@ -18,6 +18,7 @@ from konova.sub_settings.proxy_settings import *
from konova.sub_settings.sso_settings import *
from konova.sub_settings.table_settings import *
from konova.sub_settings.lanis_settings import *
from konova.sub_settings.wfs_parcel_settings import *
from konova.sub_settings.logging_settings import *
# Max upload size for POST forms

78
konova/sso/sso.py Normal file
View File

@@ -0,0 +1,78 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.08.21
"""
from django.http import HttpResponse
from django.urls import re_path
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from itsdangerous import TimedSerializer
from simple_sso.sso_client.client import Client
from user.models import User
class PropagateView(View):
""" View used to receive propagated sso-server user data
"""
client = None
signer = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.signer = TimedSerializer(self.client.private_key)
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def post(self, request):
user_data = request.body
user_data = self.signer.loads(user_data)
self.client.build_user(user_data)
return HttpResponse(status=200)
class KonovaSSOClient(Client):
""" Konova specialized derivative of general sso.Client.
Adds some custom behaviour for konova usage.
"""
propagate_view = PropagateView
def get_urls(self):
urls = super().get_urls()
urls += re_path(r'^propagate/$', self.propagate_view.as_view(client=self), name='simple-sso-propagate'),
return urls
def build_user(self, user_data):
""" Creates a user or updates user data
Args:
user_data ():
Returns:
"""
try:
user = User.objects.get(username=user_data['username'])
# Update user data, excluding some changes
skipable_attrs = {
"username",
"is_staff",
"is_superuser",
}
for _attr, _val in user_data.items():
if _attr in skipable_attrs:
continue
setattr(user, _attr, _val)
except User.DoesNotExist:
user = User(**user_data)
user.set_unusable_password()
user.save()
return user

View File

@@ -278,18 +278,4 @@ Similar to bootstraps 'shadow-lg'
}
.alert{
margin-bottom: 0 !important;
}
/*
Overwrites netgis.css attributes
*/
.netgis-gradient-a{
/*
Overwrites gradient used on default css of netgis map client
*/
background: var(--rlp-red) !important;
}
.netgis-menu{
z-index: 1 !important;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.6 MiB

Some files were not shown because too many files have changed in this diff Show More