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
176 changed files with 3075 additions and 6481 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/ /.idea/
/.coverage /.coverage
/htmlcov/ /htmlcov/
/.env

View File

@@ -10,6 +10,6 @@ from analysis.views import *
app_name = "analysis" app_name = "analysis"
urlpatterns = [ urlpatterns = [
path("reports/", ReportIndexView.as_view(), name="reports"), path("reports/", index_reports_view, name="reports"),
path("reports/<id>", ReportDetailView.as_view(), name="report-detail"), path("reports/<id>", detail_report_view, name="report-detail"),
] ]

View File

@@ -1,12 +1,8 @@
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, HttpResponse
from django.shortcuts import render, redirect, get_object_or_404 from django.shortcuts import render, redirect, get_object_or_404
from django.utils import timezone from django.utils import timezone
from django.utils.decorators import method_decorator
from django.views import View
from django.views.generic import DetailView
from analysis.forms import TimespanReportForm from analysis.forms import TimespanReportForm
from analysis.utils.excel.excel import TempExcelFile from analysis.utils.excel.excel import TempExcelFile
@@ -46,112 +42,57 @@ def index_reports_view(request: HttpRequest):
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, template, context)
class ReportIndexView(LoginRequiredMixin, View):
@method_decorator(conservation_office_group_required)
def get(self, request: HttpRequest) -> HttpResponse:
""" @login_required
@conservation_office_group_required
def detail_report_view(request: HttpRequest, id: str):
""" Renders the detailed report for a conservation office
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The conservation_office KonovaCode id
Returns: Returns:
""" """
template = "analysis/reports/index.html" # Try to resolve the requested office id
form = TimespanReportForm(None) cons_office = get_object_or_404(
context = { KonovaCode,
"form": form id=id
} )
context = BaseContext(request, context).context # Try to resolve the date parameters into Date objects -> redirect if this fails
return render(request, template, context) try:
df = request.GET.get("df", None)
@method_decorator(conservation_office_group_required) dt = request.GET.get("dt", None)
def post(self, request: HttpRequest) -> HttpResponse: date_from = timezone.make_aware(timezone.datetime.fromisoformat(df))
date_to = timezone.make_aware(timezone.datetime.fromisoformat(dt))
""" except ValueError:
messages.error(
Args: request,
request (HttpRequest): The incoming request PARAMS_INVALID,
extra_tags="danger",
Returns:
"""
template = "analysis/reports/index.html"
form = TimespanReportForm(request.POST or None)
if form.is_valid():
redirect_url = form.save()
return redirect(redirect_url)
else:
messages.error(
request,
FORM_INVALID,
extra_tags="danger",
)
context = {
"form": form
}
context = BaseContext(request, context).context
return render(request, template, context)
class ReportDetailView(LoginRequiredMixin, DetailView):
@method_decorator(conservation_office_group_required)
def get(self, request: HttpRequest, id: str):
""" Renders the detailed report for a conservation office
Args:
request (HttpRequest): The incoming request
id (str): The conservation_office KonovaCode id
Returns:
"""
# Try to resolve the requested office id
cons_office = get_object_or_404(
KonovaCode,
id=id
) )
# Try to resolve the date parameters into Date objects -> redirect if this fails return redirect("analysis:reports")
try:
df = request.GET.get("df", None)
dt = request.GET.get("dt", None)
date_from = timezone.make_aware(timezone.datetime.fromisoformat(df))
date_to = timezone.make_aware(timezone.datetime.fromisoformat(dt))
except ValueError:
messages.error(
request,
PARAMS_INVALID,
extra_tags="danger",
)
return redirect("analysis:reports")
# Check whether the html default rendering is requested or an alternative # Check whether the html default rendering is requested or an alternative
format_param = request.GET.get("format", "html") format_param = request.GET.get("format", "html")
report = TimespanReport(id, date_from, date_to) report = TimespanReport(id, date_from, date_to)
if format_param == "html": if format_param == "html":
return self.__handle_html_format(request, report, cons_office)
elif format_param == "excel":
return self.__handle_excel_format(report, cons_office, df, dt)
else:
raise NotImplementedError
def __handle_html_format(self, request, report: TimespanReport, office: KonovaCode):
template = "analysis/reports/detail.html" template = "analysis/reports/detail.html"
context = { context = {
"office": office, "office": cons_office,
"report": report, "report": report,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, template, context) return render(request, template, context)
elif format_param == "excel":
def __handle_excel_format(self, report: TimespanReport, office: KonovaCode, df: str, dt: str):
file = TempExcelFile(report.excel_template_path, report.excel_map) file = TempExcelFile(report.excel_template_path, report.excel_map)
response = HttpResponse( response = HttpResponse(
content=file.stream, content=file.stream,
content_type="application/ms-excel", content_type="application/ms-excel",
) )
response['Content-Disposition'] = f'attachment; filename={office.long_name}_{df}_{dt}.xlsx' response['Content-Disposition'] = f'attachment; filename={cons_office.long_name}_{df}_{dt}.xlsx'
return response return response
else:
raise NotImplementedError

View File

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

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.core.exceptions import ObjectDoesNotExist
from django.db import models from django.db import models
from django.utils import timezone from django.utils import timezone
from django.utils.timezone import now
from konova.models import UuidModel
from konova.sub_settings.sso_settings import OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET, SSO_SERVER_BASE
from konova.utils.generators import generate_token from konova.utils.generators import generate_token
@@ -51,129 +44,5 @@ class APIUserToken(models.Model):
if token_obj.valid_until is not None and token_obj.valid_until < _today: if token_obj.valid_until is not None and token_obj.valid_until < _today:
raise PermissionError("Token validity expired") raise PermissionError("Token validity expired")
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise PermissionError("Token unknown") raise PermissionError("Credentials invalid")
return token_obj.user return token_obj.user
class OAuthToken(UuidModel):
access_token = models.CharField(
max_length=255,
blank=False,
null=False,
db_comment="OAuth access token"
)
refresh_token = models.CharField(
max_length=255,
blank=False,
null=False,
db_comment="OAuth refresh token"
)
expires_on = models.DateTimeField(
db_comment="When the token will be expired"
)
ASSUMED_LATENCY = 1000 # assumed latency between creation and receiving of an access token
def __str__(self):
return str(self.access_token)
@staticmethod
def from_access_token_response(access_token_data: str, received_on):
"""
Creates an OAuthToken based on retrieved access token data (OAuth2.0 specification)
Args:
access_token_data (str): OAuth2.0 response data
received_on (): Timestamp when the response has been received
Returns:
"""
oauth_token = OAuthToken()
data = json.loads(access_token_data)
oauth_token.access_token = data.get("access_token")
oauth_token.refresh_token = data.get("refresh_token")
expires_on = received_on + timedelta(
seconds=(data.get("expires_in") + OAuthToken.ASSUMED_LATENCY)
)
oauth_token.expires_on = expires_on
return oauth_token
def refresh(self):
url = f"{SSO_SERVER_BASE}o/token/"
params = {
"grant_type": "refresh_token",
"refresh_token": self.refresh_token,
"client_id": OAUTH_CLIENT_ID,
"client_secret": OAUTH_CLIENT_SECRET
}
response = requests.post(
url,
params
)
_now = now()
is_response_invalid = response.status_code != 200
if is_response_invalid:
raise RuntimeError(f"Refreshing token not possible: {response.status_code}")
response_content = response.content.decode("utf-8")
response_content = json.loads(response_content)
access_token = response_content.get("access_token")
refresh_token = response_content.get("refresh_token")
expires_in = response_content.get("expires")
self.access_token = access_token
self.refresh_token = refresh_token
self.expires_in = expires_in
self.save()
return self
def update_and_get_user(self):
from user.models import User
url = f"{SSO_SERVER_BASE}users/oauth/data/"
access_token = self.access_token
response = requests.get(
url,
headers={
"Authorization": f"Bearer {access_token}",
}
)
is_response_code_invalid = response.status_code != 200
if is_response_code_invalid:
raise RuntimeError(f"OAuth user data fetching unsuccessful: {response.status_code}")
response_content = response.content.decode("utf-8")
response_content = json.loads(response_content)
user = User.oauth_update_user(response_content)
return user
def revoke(self) -> int:
""" Revokes the OAuth2 token of the user
(/o/revoke_token/ indeed removes the corresponding access token on provider side and invalidates the
submitted refresh token in one step)
Returns:
revocation_status_code (int): HTTP status code for revocation of refresh_token
"""
revoke_url = f"{SSO_SERVER_BASE}o/revoke_token/"
token = self.refresh_token
revocation_status_code = requests.post(
revoke_url,
data={
'token': token,
'token_type_hint': "refresh_token",
},
auth=(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET),
).status_code
return revocation_status_code

View File

@@ -71,7 +71,7 @@ class APIV1CreateTestCase(BaseAPIV1TestCase):
# Expect this first request to fail, since user has no shared access on the intervention, we want to create # Expect this first request to fail, since user has no shared access on the intervention, we want to create
# a compensation for # a compensation for
response = self._run_create_request(url, post_body) response = self._run_create_request(url, post_body)
self.assertEqual(response.status_code, 400, msg=response.content) self.assertEqual(response.status_code, 500, msg=response.content)
content = json.loads(response.content) content = json.loads(response.content)
self.assertGreater(len(content.get("errors", [])), 0, msg=response.content) self.assertGreater(len(content.get("errors", [])), 0, msg=response.content)

View File

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

View File

@@ -7,8 +7,11 @@ Created on: 21.01.22
""" """
from django.urls import path, include from django.urls import path, include
from api.views.method_views import generate_new_token_view
app_name = "api" app_name = "api"
urlpatterns = [ urlpatterns = [
path("v1/", include("api.urls.v1.urls", namespace="v1")), path("v1/", include("api.urls.v1.urls", namespace="v1")),
path("token/generate", generate_new_token_view, name="generate-new-token"),
] ]

View File

@@ -11,9 +11,8 @@ from abc import abstractmethod
from django.contrib.gis import geos from django.contrib.gis import geos
from django.contrib.gis.geos import GEOSGeometry from django.contrib.gis.geos import GEOSGeometry
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Q
from konova.models import Geometry from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
@@ -33,8 +32,8 @@ class AbstractModelAPISerializer:
self.lookup = { self.lookup = {
"id": None, # must be set "id": None, # must be set
"deleted__isnull": True, "deleted__isnull": True,
"users__in": [], # must be set
} }
self.shared_lookup = Q() # must be set, so user or team based share will be used properly
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@abstractmethod @abstractmethod
@@ -77,11 +76,7 @@ class AbstractModelAPISerializer:
else: else:
# Return certain object # Return certain object
self.lookup["id"] = _id self.lookup["id"] = _id
self.lookup["users__in"] = [user]
self.shared_lookup = Q(
Q(users__in=[user]) |
Q(teams__in=list(user.shared_teams))
)
def fetch_and_serialize(self): def fetch_and_serialize(self):
""" Serializes the model entry according to the given lookup data """ Serializes the model entry according to the given lookup data
@@ -91,13 +86,7 @@ class AbstractModelAPISerializer:
Returns: Returns:
serialized_data (dict) serialized_data (dict)
""" """
entries = self.model.objects.filter( entries = self.model.objects.filter(**self.lookup).order_by("id")
**self.lookup
).filter(
self.shared_lookup
).order_by(
"id"
).distinct()
self.paginator = Paginator(entries, self.rpp) self.paginator = Paginator(entries, self.rpp)
requested_entries = self.paginator.page(self.page_number) requested_entries = self.paginator.page(self.page_number)
@@ -145,8 +134,8 @@ class AbstractModelAPISerializer:
if isinstance(geojson, dict): if isinstance(geojson, dict):
geojson = json.dumps(geojson) geojson = json.dumps(geojson)
geometry = geos.fromstr(geojson) geometry = geos.fromstr(geojson)
geometry = Geometry.cast_to_rlp_srid(geometry) if geometry.srid != DEFAULT_SRID_RLP:
geometry = Geometry.cast_to_multipolygon(geometry) geometry.transform(DEFAULT_SRID_RLP)
return geometry return geometry
def _get_obj_from_db(self, id, user): 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 import transaction
from django.db.models import Q
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin
from compensation.models import Compensation from compensation.models import Compensation
@@ -22,10 +21,8 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
def prepare_lookup(self, id, user): def prepare_lookup(self, id, user):
super().prepare_lookup(id, user) super().prepare_lookup(id, user)
self.shared_lookup = Q( del self.lookup["users__in"]
Q(intervention__users__in=[user]) | self.lookup["intervention__users__in"] = [user]
Q(intervention__teams__in=user.shared_teams)
)
def intervention_to_json(self, entry): def intervention_to_json(self, entry):
return { return {

View File

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

View File

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

35
api/views/method_views.py Normal file
View File

@@ -0,0 +1,35 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 27.01.22
"""
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, JsonResponse
from api.models import APIUserToken
@login_required
def generate_new_token_view(request: HttpRequest):
""" Handles request for fetching
Args:
request (HttpRequest): The incoming request
Returns:
"""
if request.method == "GET":
token = APIUserToken()
while APIUserToken.objects.filter(token=token.token).exists():
token = APIUserToken()
return JsonResponse(
data={
"gen_data": token.token
}
)
else:
raise NotImplementedError

View File

@@ -6,9 +6,7 @@ Created on: 21.01.22
""" """
import json import json
from json import JSONDecodeError
from django.core.exceptions import ObjectDoesNotExist
from django.http import JsonResponse, HttpRequest from django.http import JsonResponse, HttpRequest
from api.utils.serializer.v1.compensation import CompensationAPISerializerV1 from api.utils.serializer.v1.compensation import CompensationAPISerializerV1
@@ -25,6 +23,11 @@ class AbstractAPIViewV1(AbstractAPIView):
""" """
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.lookup = {
"id": None, # must be set in subclasses
"deleted__isnull": True,
"users__in": [], # must be set in subclasses
}
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.serializer = self.serializer() self.serializer = self.serializer()
@@ -68,12 +71,8 @@ class AbstractAPIViewV1(AbstractAPIView):
body = request.body.decode("utf-8") body = request.body.decode("utf-8")
body = json.loads(body) body = json.loads(body)
created_id = self.serializer.create_model_from_json(body, self.user) created_id = self.serializer.create_model_from_json(body, self.user)
except (JSONDecodeError, except Exception as e:
AssertionError, return self._return_error_response(e, 500)
ValueError,
PermissionError,
ObjectDoesNotExist) as e:
return self._return_error_response(e, 400)
return JsonResponse({"id": created_id}) return JsonResponse({"id": created_id})
def put(self, request: HttpRequest, id=None): def put(self, request: HttpRequest, id=None):

View File

@@ -50,19 +50,14 @@ class AbstractAPIView(View):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
# Fetch the proper user from the given request header token # Fetch the proper user from the given request header token
token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None) ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None) ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
token_user = APIUserToken.get_user_from_token(ksp_token)
if not token and not ksp_user: if ksp_user != token_user.username:
bearer_token = request.headers.get("authorization", None)
if not bearer_token:
raise PermissionError("No token provided")
token = bearer_token.split(" ")[1]
token_user = APIUserToken.get_user_from_token(token)
if ksp_user and ksp_user != token_user.username:
raise PermissionError(f"Invalid token for {ksp_user}") raise PermissionError(f"Invalid token for {ksp_user}")
self.user = token_user else:
self.user = token_user
request.user = self.user request.user = self.user
if not self.user.is_default_user(): if not self.user.is_default_user():
@@ -81,7 +76,9 @@ class AbstractAPIView(View):
Returns: Returns:
""" """
content = [f"{error.__class__.__name__}: {str(error)}"] content = [error.__str__()]
if hasattr(error, "messages"):
content = error.messages
return JsonResponse( return JsonResponse(
{ {
"errors": content "errors": content

View File

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

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_REGISTRATION_OFFICE_ID, CODELIST_BIOTOPES_ID, CODELIST_LAW_ID, CODELIST_HANDLER_ID, \
CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID, \ CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_CLASS_ID, CODELIST_COMPENSATION_ADDITIONAL_TYPE_ID, \
CODELIST_BASE_URL, CODELIST_PROCESS_TYPE_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \ CODELIST_BASE_URL, CODELIST_PROCESS_TYPE_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID CODELIST_COMPENSATION_ACTION_DETAIL_ID
from konova.management.commands.setup import BaseKonovaCommand from konova.management.commands.setup import BaseKonovaCommand
from konova.settings import PROXIES from konova.settings import PROXIES
@@ -34,7 +34,6 @@ class Command(BaseKonovaCommand):
CODELIST_REGISTRATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID,
CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_ID,
CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID,
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID,
CODELIST_LAW_ID, CODELIST_LAW_ID,
CODELIST_HANDLER_ID, CODELIST_HANDLER_ID,
CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_ID,
@@ -56,7 +55,7 @@ class Command(BaseKonovaCommand):
content = result.content.decode("utf-8") content = result.content.decode("utf-8")
root = etree.fromstring(content) root = etree.fromstring(content)
items = root.findall("item") items = root.findall("item")
self._write_warning(" Found {} codes. Process now...".format(len(items))) self._write_warning("Found {} codes. Process now...".format(len(items)))
code_list = KonovaCodeList.objects.get_or_create( code_list = KonovaCodeList.objects.get_or_create(
id=list_id, id=list_id,
@@ -75,15 +74,15 @@ class Command(BaseKonovaCommand):
if items is None: if items is None:
return return
else: else:
self._write_warning(" --- Found {} subcodes. Process now...".format(len(items))) self._write_warning(" --- Found {} subcodes. Process now...".format(len(items)))
for element in items: for element in items:
children = element.find("items") children = element.find("items")
_id = element.find("id").text _id = element.find("id").text
atom_id = element.find("atomid").text atom_id = element.find("atomid").text
selectable = element.find("selectable").text.lower() selectable = element.find("selectable").text.lower()
selectable = bool_map.get(selectable, False) selectable = bool_map.get(selectable, False)
short_name = element.find("shortname").text or "" short_name = element.find("shortname").text
long_name = element.find("longname").text or "" long_name = element.find("longname").text
is_archived = bool_map.get((element.find("archive").text.lower()), False) is_archived = bool_map.get((element.find("archive").text.lower()), False)
code = KonovaCode.objects.get_or_create( code = KonovaCode.objects.get_or_create(

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( short_name = models.CharField(
max_length=500, max_length=500,
null=True,
blank=True, blank=True,
help_text="Short version of long name", help_text="Short version of long name",
) )
long_name = models.CharField( long_name = models.CharField(
max_length=1000, max_length=1000,
null=True,
blank=True, blank=True,
help_text="", help_text="",
) )
@@ -48,28 +50,12 @@ class KonovaCode(models.Model):
def __str__(self, with_parent: bool = True): def __str__(self, with_parent: bool = True):
ret_val = "" ret_val = ""
if self.parent and self.parent.long_name and with_parent:
long_name = self.long_name ret_val += self.parent.long_name + " > "
short_name = self.short_name ret_val += self.long_name
if self.short_name and self.short_name != self.long_name:
both_names_exist = long_name is not None and short_name is not None # Only add short name, if we won't have stupid repition like 'thing a (thing a)' due to misused long-short names
ret_val += f" ({self.short_name})"
if both_names_exist:
if with_parent and self.parent:
parent_short_name_exists = self.parent.short_name is not None
parent_long_name_exists = self.parent.long_name is not None
if parent_long_name_exists:
ret_val += self.parent.long_name + " > "
elif parent_short_name_exists:
ret_val += self.parent.short_name + " > "
ret_val += long_name
if short_name and short_name != long_name:
ret_val += f" ({short_name})"
else:
ret_val += str(long_name or short_name)
return ret_val return ret_val
@property @property
@@ -89,8 +75,7 @@ class KonovaCode(models.Model):
return self return self
children = KonovaCode.objects.filter( children = KonovaCode.objects.filter(
parent=self, parent=self
is_archived=False,
).order_by( ).order_by(
order_by order_by
) )

View File

@@ -15,8 +15,7 @@ CODELIST_CONSERVATION_OFFICE_ID = 907 # CLNaturschutzbehörden
CODELIST_REGISTRATION_OFFICE_ID = 1053 # CLZulassungsbehörden CODELIST_REGISTRATION_OFFICE_ID = 1053 # CLZulassungsbehörden
CODELIST_BIOTOPES_ID = 654 # CL_Biotoptypen CODELIST_BIOTOPES_ID = 654 # CL_Biotoptypen
CODELIST_AFTER_STATE_BIOTOPES_ID = 974 # CL-KSP_ZielBiotoptypen - USAGE HAS BEEN DROPPED IN 2022 IN FAVOR OF 654 CODELIST_AFTER_STATE_BIOTOPES_ID = 974 # CL-KSP_ZielBiotoptypen - USAGE HAS BEEN DROPPED IN 2022 IN FAVOR OF 654
CODELIST_BIOTOPES_EXTRA_CODES_ID = 975 # CLZusatzbezeichnung - Subset of 288. Migration usage 975->288 in 08/2024 CODELIST_BIOTOPES_EXTRA_CODES_ID = 975 # CLZusatzbezeichnung
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID = 288 # CLBiotoptypZusatzcode
CODELIST_LAW_ID = 1048 # CLVerfahrensrecht CODELIST_LAW_ID = 1048 # CLVerfahrensrecht
CODELIST_PROCESS_TYPE_ID = 44382 # CLVerfahrenstyp CODELIST_PROCESS_TYPE_ID = 44382 # CLVerfahrenstyp

View File

@@ -45,14 +45,6 @@ class AbstractCompensationAdmin(BaseObjectAdmin):
states = "\n".join(states) states = "\n".join(states)
return states return states
def get_actions(self, request):
DELETE_ACTION_IDENTIFIER = "delete_selected"
actions = super().get_actions(request)
if DELETE_ACTION_IDENTIFIER in actions:
del actions[DELETE_ACTION_IDENTIFIER]
return actions
class CompensationAdmin(AbstractCompensationAdmin): class CompensationAdmin(AbstractCompensationAdmin):
autocomplete_fields = [ autocomplete_fields = [

View File

@@ -15,7 +15,6 @@ from compensation.models import EcoAccount
from intervention.models import Handler, Responsibility, Legal from intervention.models import Handler, Responsibility, Legal
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm from konova.forms.modals import RemoveModalForm
from konova.settings import ETS_GROUP
from konova.utils import validators from konova.utils import validators
from user.models import User, UserActionLogEntry from user.models import User, UserActionLogEntry
@@ -247,13 +246,4 @@ class RemoveEcoAccountModalForm(RemoveModalForm):
"confirm", "confirm",
_("The account can not be removed, since there are still deductions.") _("The account can not be removed, since there are still deductions.")
) )
# If there are deductions but the performing user is not part of an ETS group, we assume this poor
# fella does not know what he/she does -> give a hint that they should contact someone in charge...
user_is_ets_user = self.user.in_group(ETS_GROUP)
if not user_is_ets_user:
self.add_error(
"confirm",
_("Please contact the responsible conservation office to find a solution!")
)
return super_valid and not has_deductions return super_valid and not has_deductions

View File

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

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 import models
from django.db.models import Q
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, \ from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from compensation.managers import CompensationStateManager from compensation.managers import CompensationStateManager
from konova.models import UuidModel from konova.models import UuidModel
@@ -34,7 +34,7 @@ class CompensationState(UuidModel):
KonovaCode, KonovaCode,
blank=True, blank=True,
limit_choices_to={ limit_choices_to={
"code_lists__in": [CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID], "code_lists__in": [CODELIST_BIOTOPES_EXTRA_CODES_ID],
"is_selectable": True, "is_selectable": True,
"is_archived": False, "is_archived": False,
}, },

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -10,7 +10,7 @@ from compensation.views.payment import *
app_name = "pay" app_name = "pay"
urlpatterns = [ urlpatterns = [
path('<id>/new', NewPaymentView.as_view(), name='new'), path('<id>/new', new_payment_view, name='new'),
path('<id>/remove/<payment_id>', RemovePaymentView.as_view(), name='remove'), path('<id>/remove/<payment_id>', payment_remove_view, name='remove'),
path('<id>/edit/<payment_id>', EditPaymentView.as_view(), name='edit'), path('<id>/edit/<payment_id>', payment_edit_view, name='edit'),
] ]

View File

@@ -6,127 +6,91 @@ Created on: 19.08.22
""" """
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse from django.core.exceptions import ObjectDoesNotExist
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, render, redirect
from django.utils.decorators import method_decorator from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View
from compensation.forms.compensation import EditCompensationForm, NewCompensationForm from compensation.forms.compensation import EditCompensationForm, NewCompensationForm
from compensation.models import Compensation from compensation.models import Compensation
from compensation.tables.compensation import CompensationTable from compensation.tables.compensation import CompensationTable
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, \ from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
IDENTIFIER_REPLACED, COMPENSATION_ADDED_TEMPLATE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \
from konova.views.identifier import AbstractIdentifierGeneratorView COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
from konova.views.index import AbstractIndexView
class IndexCompensationView(AbstractIndexView): @login_required
def get(self, request, *args, **kwargs) -> HttpResponse: @any_group_check
""" def index_view(request: HttpRequest):
Renders the index view for compensation """
Renders the index view for compensation
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
Returns: Returns:
A rendered view A rendered view
""" """
compensations = Compensation.objects.filter( template = "generic_index.html"
deleted=None, # only show those which are not deleted individually compensations = Compensation.objects.filter(
intervention__deleted=None, # and don't show the ones whose intervention has been deleted deleted=None, # only show those which are not deleted individually
).order_by( intervention__deleted=None, # and don't show the ones whose intervention has been deleted
"-modified__timestamp" ).order_by(
) "-modified__timestamp"
table = CompensationTable( )
request=request, table = CompensationTable(
queryset=compensations request=request,
) queryset=compensations
context = { )
"table": table, context = {
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"), "table": table,
} TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
context = BaseContext(request, context).context }
return render(request, self._TEMPLATE, context) context = BaseContext(request, context).context
return render(request, template, context)
class NewCompensationView(LoginRequiredMixin, View): @login_required
_TEMPLATE = "compensation/form/view.html" @default_group_required
@shared_access_required(Intervention, "intervention_id")
def new_view(request: HttpRequest, intervention_id: str = None):
"""
Renders a view for a new compensation creation
@method_decorator(default_group_required) Args:
@method_decorator(shared_access_required(Intervention, "intervention_id")) request (HttpRequest): The incoming request
def get(self, request: HttpRequest, intervention_id: str = None, *args, **kwargs) -> HttpResponse:
"""
Renders a view for new compensation
A compensation creation may be called directly from the parent-intervention object. If so - we may take Returns:
the intervention's id and directly link the compensation to it.
Args: """
request (HttpRequest): The incoming request template = "compensation/form/view.html"
intervention_id (str): The intervention identifier if intervention_id is not None:
try:
Returns: intervention = Intervention.objects.get(id=intervention_id)
except ObjectDoesNotExist:
""" messages.error(request, PARAMS_INVALID)
if intervention_id: return redirect("home")
# If the parent-intervention is recorded, we are not allowed to change anything on it's data. if intervention.is_recorded:
# Not even adding new child elements like compensations! messages.info(
intervention = get_object_or_404(Intervention, id=intervention_id) request,
recording_state_blocks_actions = intervention.is_recorded RECORDED_BLOCKS_EDIT
if recording_state_blocks_actions: )
messages.info( return redirect("intervention:detail", id=intervention_id)
request,
RECORDED_BLOCKS_EDIT
)
return redirect("intervention:detail", id=intervention_id)
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New compensation"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "intervention_id"))
def post(self, request: HttpRequest, intervention_id: str = None, *args, **kwargs) -> HttpResponse:
"""
Renders a view for a new compensation creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
if intervention_id:
# If the parent-intervention is recorded, we are not allowed to change anything on it's data.
# Not even adding new child elements like compensations!
intervention = get_object_or_404(Intervention, id=intervention_id)
recording_state_blocks_actions = intervention.is_recorded
if recording_state_blocks_actions:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("intervention:detail", id=intervention_id)
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid(): if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None) generated_identifier = data_form.cleaned_data.get("identifier", None)
comp = data_form.save(request.user, geom_form) comp = data_form.save(request.user, geom_form)
@@ -139,127 +103,199 @@ class NewCompensationView(LoginRequiredMixin, View):
) )
) )
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier)) messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
if geom_form.has_geometry_simplified(): if geom_form.geometry_simplified:
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED GEOMETRY_SIMPLIFIED
) )
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("compensation:detail", id=comp.id) return redirect("compensation:detail", id=comp.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger", ) messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New compensation"),
}
context = BaseContext(request, context).context
return render(request, template, context)
context = {
"form": data_form, @login_required
"geom_form": geom_form, @default_group_required
TAB_TITLE_IDENTIFIER: _("New compensation"), 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
} }
context = BaseContext(request, context).context )
return render(request, self._TEMPLATE, context)
class CompensationIdentifierGeneratorView(AbstractIdentifierGeneratorView): @login_required
_MODEL = Compensation @default_group_required
@shared_access_required(Compensation, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
class EditCompensationView(LoginRequiredMixin, View): Returns:
_TEMPLATE = "compensation/form/view.html"
@method_decorator(default_group_required) """
@method_decorator(shared_access_required(Compensation, "id")) template = "compensation/form/view.html"
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: # Get object from db
comp = get_object_or_404(Compensation, id=id)
if comp.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:detail", id=id)
""" # Create forms, initialize with values from db/from POST request
Renders a view for editing compensations data_form = EditCompensationForm(request.POST or None, instance=comp)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
Args: if request.method == "POST":
request (HttpRequest): The incoming request
Returns:
"""
# Get object from db
comp = get_object_or_404(Compensation, id=id)
if comp.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditCompensationForm(request.POST or None, instance=comp)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
# Get object from db
comp = get_object_or_404(Compensation, id=id)
if comp.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditCompensationForm(request.POST or None, instance=comp)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
if data_form.is_valid() and geom_form.is_valid(): if data_form.is_valid() and geom_form.is_valid():
# Preserve state of intervention checked to determine whether the user must be informed or not # Preserve state of intervention checked to determine whether the user must be informed or not
# about a change of the check state # about a change of the check state
intervention_is_checked = comp.intervention.checked is not None intervention_is_checked = comp.intervention.checked is not None
# The data form takes the geom form for processing, as well as the performing user # The data form takes the geom form for processing, as well as the performing user
comp = data_form.save(request.user, geom_form) comp = data_form.save(request.user, geom_form)
if intervention_is_checked: if intervention_is_checked:
messages.info(request, CHECK_STATE_RESET) messages.info(request, CHECK_STATE_RESET)
messages.success(request, _("Compensation {} edited").format(comp.identifier)) messages.success(request, _("Compensation {} edited").format(comp.identifier))
if geom_form.has_geometry_simplified(): if geom_form.geometry_simplified:
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED GEOMETRY_SIMPLIFIED
) )
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("compensation:detail", id=comp.id) return redirect("compensation:detail", id=comp.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger", ) messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier),
}
context = BaseContext(request, context).context
return render(request, template, context)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier),
}
context = BaseContext(request, context).context
return render(request, self._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 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.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -14,72 +14,67 @@ from compensation.models import Compensation
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.qrcode import QrCode from konova.utils.generators import generate_qr_code
from konova.views.report import AbstractPublicReportView
class CompensationPublicReportView(AbstractPublicReportView): def report_view(request: HttpRequest, id: str):
_TEMPLATE = "compensation/report/compensation/report.html" """ Renders the public report view
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: Args:
""" Renders the public report view request (HttpRequest): The incoming request
id (str): The id of the intervention
Args: Returns:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns: """
# Reuse the compensation report template since compensations are structurally identical
""" template = "compensation/report/compensation/report.html"
comp = get_object_or_404(Compensation, id=id) comp = get_object_or_404(Compensation, id=id)
tab_title = _("Report {}").format(comp.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not comp.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=comp
)
parcels = comp.get_underlying_parcels()
qrcode = QrCode(
content=request.build_absolute_uri(reverse("compensation:report", args=(id,))),
size=10
)
qrcode_lanis = QrCode(
content=comp.get_LANIS_link(),
size=7
)
# Order states by surface
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type")
actions = comp.actions.all().prefetch_related("action_type")
tab_title = _("Report {}").format(comp.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not comp.is_ready_for_publish():
template = "report/unavailable.html"
context = { context = {
"obj": comp,
"qrcode": {
"img": qrcode.get_img(),
"url": qrcode.get_content(),
},
"qrcode_lanis": {
"img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(),
},
"is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, 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

@@ -6,94 +6,72 @@ Created on: 19.08.22
""" """
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin 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.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.utils.translation import gettext_lazy as _
from django.views import View
from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm, RemoveEcoAccountModalForm
from compensation.models import EcoAccount from compensation.models import EcoAccount
from compensation.tables.eco_account import EcoAccountTable from compensation.tables.eco_account import EcoAccountTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, FORM_INVALID, \ from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.index import AbstractIndexView
class IndexEcoAccountView(AbstractIndexView): @login_required
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: @any_group_check
""" def index_view(request: HttpRequest):
Renders the index view for eco accounts """
Renders the index view for eco accounts
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
Returns: Returns:
A rendered view A rendered view
""" """
eco_accounts = EcoAccount.objects.filter( template = "generic_index.html"
deleted=None, eco_accounts = EcoAccount.objects.filter(
).order_by( deleted=None,
"-modified__timestamp" ).order_by(
) "-modified__timestamp"
table = EcoAccountTable( )
request=request, table = EcoAccountTable(
queryset=eco_accounts request=request,
) queryset=eco_accounts
context = { )
"table": table, context = {
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"), "table": table,
} TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
context = BaseContext(request, context).context }
return render(request, self._TEMPLATE, context) context = BaseContext(request, context).context
return render(request, template, context)
class NewEcoAccountView(LoginRequiredMixin, View): @login_required
_TEMPLATE = "compensation/form/view.html" @default_group_required
def new_view(request: HttpRequest):
"""
Renders a view for a new eco account creation
@method_decorator(default_group_required) Args:
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: request (HttpRequest): The incoming request
"""
Renders a view for a new eco account creation
Args: Returns:
request (HttpRequest): The incoming request
Returns: """
template = "compensation/form/view.html"
""" data_form = NewEcoAccountForm(request.POST or None)
data_form = NewEcoAccountForm(request.POST or None) geom_form = SimpleGeomForm(request.POST or None, read_only=False)
geom_form = SimpleGeomForm(request.POST or None, read_only=False) if request.method == "POST":
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New Eco-Account"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(default_group_required)
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""
Renders a view for a new eco account creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
data_form = NewEcoAccountForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if data_form.is_valid() and geom_form.is_valid(): if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None) generated_identifier = data_form.cleaned_data.get("identifier", None)
acc = data_form.save(request.user, geom_form) acc = data_form.save(request.user, geom_form)
@@ -106,123 +84,208 @@ class NewEcoAccountView(LoginRequiredMixin, View):
) )
) )
messages.success(request, _("Eco-Account {} added").format(acc.identifier)) messages.success(request, _("Eco-Account {} added").format(acc.identifier))
if geom_form.has_geometry_simplified(): if geom_form.geometry_simplified:
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED GEOMETRY_SIMPLIFIED
) )
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("compensation:acc:detail", id=acc.id) return redirect("compensation:acc:detail", id=acc.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger", ) messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New Eco-Account"),
}
context = BaseContext(request, context).context
return render(request, template, context)
context = {
"form": data_form, @login_required
"geom_form": geom_form, @default_group_required
TAB_TITLE_IDENTIFIER: _("New Eco-Account"), 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
} }
context = BaseContext(request, context).context )
return render(request, self._TEMPLATE, context)
class EcoAccountIdentifierGeneratorView(AbstractIdentifierGeneratorView):
_MODEL = EcoAccount
class EditEcoAccountView(LoginRequiredMixin, View): @login_required
_TEMPLATE = "compensation/form/view.html" @default_group_required
@shared_access_required(EcoAccount, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
@method_decorator(default_group_required) Args:
@method_decorator(shared_access_required(EcoAccount, "id")) request (HttpRequest): The incoming request
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" Returns:
Renders a view for editing compensations
Args: """
request (HttpRequest): The incoming request template = "compensation/form/view.html"
# Get object from db
Returns: acc = get_object_or_404(EcoAccount, id=id)
if acc.is_recorded:
""" messages.info(
# Get object from db request,
acc = get_object_or_404(EcoAccount, id=id) RECORDED_BLOCKS_EDIT
if acc.is_recorded: )
messages.info( return redirect("compensation:acc:detail", id=id)
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:acc:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
# Get object from db
acc = get_object_or_404(EcoAccount, id=id)
if acc.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("compensation:acc:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
# Create forms, initialize with values from db/from POST request
data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
if request.method == "POST":
data_form_valid = data_form.is_valid() data_form_valid = data_form.is_valid()
geom_form_valid = geom_form.is_valid() geom_form_valid = geom_form.is_valid()
if data_form_valid and geom_form_valid: if data_form_valid and geom_form_valid:
# The data form takes the geom form for processing, as well as the performing user # The data form takes the geom form for processing, as well as the performing user
acc = data_form.save(request.user, geom_form) acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier)) messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
if geom_form.has_geometry_simplified(): if geom_form.geometry_simplified:
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED GEOMETRY_SIMPLIFIED
) )
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("compensation:acc:detail", id=acc.id) return redirect("compensation:acc:detail", id=acc.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger", ) messages.error(request, FORM_INVALID, extra_tags="danger",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier),
}
context = BaseContext(request, context).context
return render(request, template, context)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier),
}
context = BaseContext(request, context).context
return render(request, self._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 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.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -14,79 +14,74 @@ from compensation.models import EcoAccount
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.qrcode import QrCode from konova.utils.generators import generate_qr_code
from konova.views.report import AbstractPublicReportView
class EcoAccountPublicReportView(AbstractPublicReportView): def report_view(request: HttpRequest, id: str):
_TEMPLATE = "compensation/report/eco_account/report.html" """ Renders the public report view
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: Args:
""" Renders the public report view request (HttpRequest): The incoming request
id (str): The id of the intervention
Args: Returns:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns: """
# Reuse the compensation report template since EcoAccounts are structurally identical
""" template = "compensation/report/eco_account/report.html"
acc = get_object_or_404(EcoAccount, id=id) acc = get_object_or_404(EcoAccount, id=id)
tab_title = _("Report {}").format(acc.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not acc.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=acc
)
parcels = acc.get_underlying_parcels()
qrcode = QrCode(
content=request.build_absolute_uri(reverse("compensation:acc:report", args=(id,))),
size=10
)
qrcode_lanis = QrCode(
content=acc.get_LANIS_link(),
size=7
)
# Order states by surface
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent")
after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent")
actions = acc.actions.all().prefetch_related("action_type__parent")
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier)
deductions = acc.deductions.all() \
.distinct("intervention") \
.select_related("intervention") \
.values_list("intervention__id", "intervention__identifier", "intervention__title", named=True)
tab_title = _("Report {}").format(acc.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not acc.is_ready_for_publish():
template = "report/unavailable.html"
context = { context = {
"obj": acc,
"qrcode": {
"img": qrcode.get_img(),
"url": qrcode.get_content(),
},
"qrcode_lanis": {
"img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(),
},
"is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"deductions": deductions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, 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

@@ -5,12 +5,10 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 09.08.21 Created on: 09.08.21
""" """
from django.contrib.auth.mixins import LoginRequiredMixin
from django.urls import reverse from django.urls import reverse
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.views import View
from compensation.forms.modals.payment import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm from compensation.forms.modals.payment import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm
from compensation.models import Payment from compensation.models import Payment
@@ -19,97 +17,72 @@ from konova.decorators import default_group_required, shared_access_required
from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED
class NewPaymentView(LoginRequiredMixin, View): @login_required
@default_group_required
@shared_access_required(Intervention, "id")
def new_payment_view(request: HttpRequest, id: str):
""" Renders a modal view for adding new payments
def __process_request(self, request: HttpRequest, id: str): Args:
""" Renders a modal view for adding new payments request (HttpRequest): The incoming request
id (str): The intervention's id for which a new payment shall be added
Args: Returns:
request (HttpRequest): The incoming request
id (str): The intervention's id for which a new payment shall be added
Returns: """
intervention = get_object_or_404(Intervention, id=id)
""" form = NewPaymentForm(request.POST or None, instance=intervention, request=request)
intervention = get_object_or_404(Intervention, id=id) return form.process_request(
form = NewPaymentForm(request.POST or None, instance=intervention, request=request) request,
return form.process_request( msg_success=PAYMENT_ADDED,
request, redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
msg_success=PAYMENT_ADDED, )
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str):
return self.__process_request(request, id=id)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str):
return self.__process_request(request, id=id)
class RemovePaymentView(LoginRequiredMixin, View): @login_required
@default_group_required
@shared_access_required(Intervention, "id")
def payment_remove_view(request: HttpRequest, id: str, payment_id: str):
""" Renders a modal view for removing payments
def __process_request(self, request: HttpRequest, id: str, payment_id: str): Args:
""" Renders a modal view for removing payments request (HttpRequest): The incoming request
id (str): The intervention's id
payment_id (str): The payment's id
Args: Returns:
request (HttpRequest): The incoming request
id (str): The intervention's id
payment_id (str): The payment's id
Returns: """
intervention = get_object_or_404(Intervention, id=id)
""" payment = get_object_or_404(Payment, id=payment_id)
intervention = get_object_or_404(Intervention, id=id) form = RemovePaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request)
payment = get_object_or_404(Payment, id=payment_id) return form.process_request(
form = RemovePaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request) request=request,
return form.process_request( msg_success=PAYMENT_REMOVED,
request=request, redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data"
msg_success=PAYMENT_REMOVED, )
redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data"
)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, payment_id: str):
return self.__process_request(request, id=id, payment_id=payment_id)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str, payment_id: str):
return self.__process_request(request, id=id, payment_id=payment_id)
class EditPaymentView(LoginRequiredMixin, View): @login_required
def __process_request(self, request: HttpRequest, id: str, payment_id: str): @default_group_required
""" Renders a modal view for editing payments @shared_access_required(Intervention, "id")
def payment_edit_view(request: HttpRequest, id: str, payment_id: str):
""" Renders a modal view for editing payments
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The intervention's id id (str): The intervention's id
payment_id (str): The payment's id payment_id (str): The payment's id
Returns: Returns:
""" """
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
payment = get_object_or_404(Payment, id=payment_id) payment = get_object_or_404(Payment, id=payment_id)
form = EditPaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request) form = EditPaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request)
return form.process_request( return form.process_request(
request=request, request=request,
msg_success=PAYMENT_EDITED, msg_success=PAYMENT_EDITED,
redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data" redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data"
) )
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, payment_id: str):
return self.__process_request(request, id=id, payment_id=payment_id)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str, payment_id: str):
return self.__process_request(request, id=id, payment_id=payment_id)

View File

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

View File

@@ -11,14 +11,14 @@
{% fa5_icon 'file-alt' %} {% fa5_icon 'file-alt' %}
</button> </button>
</a> </a>
{% if is_entry_shared %} {% if has_access %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}"> <button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}">
{% fa5_icon 'bell' %} {% fa5_icon 'bell' %}
</button> </button>
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-form' obj.id %}">
{% fa5_icon 'share-alt' %}
</button>
{% if is_ets_member %} {% if is_ets_member %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-form' obj.id %}">
{% fa5_icon 'share-alt' %}
</button>
{% if obj.recorded %} {% if obj.recorded %}
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'ema:record' obj.id %}"> <button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'ema:record' obj.id %}">
{% fa5_icon 'bookmark' 'far' %} {% fa5_icon 'bookmark' 'far' %}
@@ -28,21 +28,19 @@
{% fa5_icon 'bookmark' %} {% fa5_icon 'bookmark' %}
</button> </button>
{% endif %} {% endif %}
<a href="{% url 'ema:edit' obj.id %}" class="mr-2">
<button class="btn btn-default" title="{% trans 'Edit' %}">
{% fa5_icon 'edit' %}
</button>
</a>
{% endif %} {% endif %}
{% if is_default_member %} {% if is_default_member %}
<button class="btn btn-default btn-modal mr-2" data-form-url="{% url 'ema:log' obj.id %}" title="{% trans 'Show log' %}"> <a href="{% url 'ema:edit' obj.id %}" class="mr-2">
{% fa5_icon 'history' %} <button class="btn btn-default" title="{% trans 'Edit' %}">
</button> {% fa5_icon 'edit' %}
{% endif %}
{% if is_ets_member %}
<button class="btn btn-default btn-modal" data-form-url="{% url 'ema:remove' obj.id %}" title="{% trans 'Delete' %}">
{% fa5_icon 'trash' %}
</button> </button>
</a>
<button class="btn btn-default btn-modal mr-2" data-form-url="{% url 'ema:log' obj.id %}" title="{% trans 'Show log' %}">
{% fa5_icon 'history' %}
</button>
<button class="btn btn-default btn-modal" data-form-url="{% url 'ema:remove' obj.id %}" title="{% trans 'Delete' %}">
{% fa5_icon 'trash' %}
</button>
{% endif %} {% endif %}
{% endif %} {% endif %}
</div> </div>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,7 +46,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
post_data = { post_data = {
"identifier": test_id, "identifier": test_id,
"title": test_title, "title": test_title,
"output": geom_json, "geom": geom_json,
"conservation_office": test_conservation_office.id "conservation_office": test_conservation_office.id
} }
self.client_user.post(new_url, post_data) self.client_user.post(new_url, post_data)
@@ -84,7 +84,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
new_title = self.create_dummy_string() new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string() new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string() new_comment = self.create_dummy_string()
new_geometry = 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() test_conservation_office = self.get_conservation_office_code()
check_on_elements = { check_on_elements = {
@@ -99,7 +99,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
"identifier": new_identifier, "identifier": new_identifier,
"title": new_title, "title": new_title,
"comment": new_comment, "comment": new_comment,
"output": self.create_geojson(new_geometry), "geom": new_geometry.geojson,
"conservation_office": test_conservation_office.id "conservation_office": test_conservation_office.id
} }
self.client_user.post(url, post_data) self.client_user.post(url, post_data)

View File

@@ -48,7 +48,7 @@ class NewEmaFormTestCase(BaseTestCase):
) )
geom_form_data = json.loads(geom_form_data) geom_form_data = json.loads(geom_form_data)
geom_form_data = { geom_form_data = {
"output": json.dumps(geom_form_data) "geom": json.dumps(geom_form_data)
} }
geom_form = SimpleGeomForm(geom_form_data) geom_form = SimpleGeomForm(geom_form_data)
@@ -116,7 +116,7 @@ class EditEmaFormTestCase(BaseTestCase):
) )
geom_form_data = json.loads(geom_form_data) geom_form_data = json.loads(geom_form_data)
geom_form_data = { geom_form_data = {
"output": json.dumps(geom_form_data) "geom": json.dumps(geom_form_data)
} }
geom_form = SimpleGeomForm(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.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView
from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView
from ema.views.detail import DetailEmaView
from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView
from ema.views.ema import 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.log import EmaLogView
from ema.views.record import EmaRecordView from ema.views.record import EmaRecordView
from ema.views.remove import RemoveEmaView from ema.views.report import report_view
from ema.views.report import EmaPublicReportView
from ema.views.resubmission import EmaResubmissionView from ema.views.resubmission import EmaResubmissionView
from ema.views.share import EmaShareFormView, EmaShareByTokenView from ema.views.share import EmaShareFormView, EmaShareByTokenView
from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView
app_name = "ema" app_name = "ema"
urlpatterns = [ urlpatterns = [
path("", IndexEmaView.as_view(), name="index"), path("", index_view, name="index"),
path("new/", NewEmaView.as_view(), name="new"), path("new/", new_view, name="new"),
path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"), path("new/id", new_id_view, name="new-id"),
path("<id>", DetailEmaView.as_view(), name="detail"), path("<id>", detail_view, name="detail"),
path('<id>/log', EmaLogView.as_view(), name='log'), path('<id>/log', EmaLogView.as_view(), name='log'),
path('<id>/edit', EditEmaView.as_view(), name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', RemoveEmaView.as_view(), name='remove'), path('<id>/remove', remove_view, name='remove'),
path('<id>/record', EmaRecordView.as_view(), name='record'), 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>/resub', EmaResubmissionView.as_view(), name='resubmission-create'),
path('<id>/state/new', NewEmaStateView.as_view(), name='new-state'), path('<id>/state/new', NewEmaStateView.as_view(), name='new-state'),

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 import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.db.models import Sum
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render from django.shortcuts import get_object_or_404, redirect, render
from django.utils.decorators import method_decorator from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views.generic.base import View
from ema.forms import NewEmaForm, EditEmaForm from ema.forms import NewEmaForm, EditEmaForm
from ema.models import Ema from ema.models import Ema
from ema.tables import EmaTable from ema.tables import EmaTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, conservation_office_group_required from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \ from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.index import AbstractIndexView
class IndexEmaView(AbstractIndexView): @login_required
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: def index_view(request: HttpRequest):
""" Renders the index view for EMAs """ Renders the index view for EMAs
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
Returns: Returns:
""" """
emas = Ema.objects.filter( template = "generic_index.html"
deleted=None, emas = Ema.objects.filter(
).order_by( deleted=None,
"-modified__timestamp" ).order_by(
) "-modified__timestamp"
)
table = EmaTable( table = EmaTable(
request, request,
queryset=emas queryset=emas
) )
context = { context = {
"table": table, "table": table,
TAB_TITLE_IDENTIFIER: _("EMAs - Overview"), TAB_TITLE_IDENTIFIER: _("EMAs - Overview"),
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context) return render(request, template, context)
class NewEmaView(LoginRequiredMixin, View):
_TEMPLATE = "ema/form/view.html"
@method_decorator(conservation_office_group_required) @login_required
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: @conservation_office_group_required
""" GET endpoint 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: Returns:
request (HttpRequest): The incoming request
*args ():
**kwargs ():
Returns: """
template = "ema/form/view.html"
""" data_form = NewEmaForm(request.POST or None)
data_form = NewEmaForm(request.POST or None) geom_form = SimpleGeomForm(request.POST or None, read_only=False)
geom_form = SimpleGeomForm(request.POST or None, read_only=False) if request.method == "POST":
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New EMA"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(conservation_office_group_required)
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
""" POST endpoint
Processes submitted form
Args:
request (HttpRequest): The incoming request
*args ():
**kwargs ():
Returns:
"""
data_form = NewEmaForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if data_form.is_valid() and geom_form.is_valid(): if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None) generated_identifier = data_form.cleaned_data.get("identifier", None)
ema = data_form.save(request.user, geom_form) ema = data_form.save(request.user, geom_form)
@@ -109,124 +84,176 @@ class NewEmaView(LoginRequiredMixin, View):
) )
) )
messages.success(request, _("EMA {} added").format(ema.identifier)) messages.success(request, _("EMA {} added").format(ema.identifier))
if geom_form.has_geometry_simplified(): if geom_form.geometry_simplified:
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED GEOMETRY_SIMPLIFIED
) )
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("ema:detail", id=ema.id) return redirect("ema:detail", id=ema.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)
context = { else:
"form": data_form, # For clarification: nothing in this case
"geom_form": geom_form, pass
TAB_TITLE_IDENTIFIER: _("New EMA"), 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) @login_required
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: @uuid_required
return super().get(request, *args, **kwargs) def detail_view(request: HttpRequest, id: str):
""" Renders the detail view of an EMA
class EditEmaView(LoginRequiredMixin, View): Args:
_TEMPLATE = "compensation/form/view.html" request (HttpRequest): The incoming request
id (str): The EMA id
@method_decorator(conservation_office_group_required) Returns:
@method_decorator(shared_access_required(Ema, "id"))
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
""" GET endpoint
Renders form """
template = "ema/detail/view.html"
ema = get_object_or_404(Ema, id=id, deleted=None)
Args: geom_form = SimpleGeomForm(instance=ema)
request (HttpRequest): The incoming request parcels = ema.get_underlying_parcels()
id (str): The ema identifier _user = request.user
*args (): is_data_shared = ema.is_shared_with(_user)
**kwargs ():
Returns: # 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
# Get object from db # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
ema = get_object_or_404(Ema, id=id) sum_before_states = ema.get_surface_before_states()
if ema.is_recorded: sum_after_states = ema.get_surface_after_states()
messages.info( diff_states = abs(sum_before_states - sum_after_states)
request,
RECORDED_BLOCKS_EDIT
)
return redirect("ema:detail", id=id)
# Create forms, initialize with values from db/from POST request ema.set_status_messages(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)
@method_decorator(conservation_office_group_required) requesting_user_is_only_shared_user = ema.is_only_shared_with(_user)
@method_decorator(shared_access_required(Ema, "id")) if requesting_user_is_only_shared_user:
def post(self, request: HttpRequest, id:str, *args, **kwargs) -> HttpResponse: messages.info(
""" POST endpoint 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
""" Args:
# Get object from db request (HttpRequest): The incoming request
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 Returns:
data_form = EditEmaForm(request.POST or None, instance=ema)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema) """
template = "compensation/form/view.html"
# Get object from db
ema = get_object_or_404(Ema, id=id)
if ema.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("ema:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEmaForm(request.POST or None, instance=ema)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid(): if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user # The data form takes the geom form for processing, as well as the performing user
ema = data_form.save(request.user, geom_form) ema = data_form.save(request.user, geom_form)
messages.success(request, _("EMA {} edited").format(ema.identifier)) messages.success(request, _("EMA {} edited").format(ema.identifier))
if geom_form.has_geometry_simplified(): if geom_form.geometry_simplified:
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED GEOMETRY_SIMPLIFIED
) )
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("ema:detail", id=ema.id) return redirect("ema:detail", id=ema.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger", ) messages.error(request, FORM_INVALID, extra_tags="danger",)
context = { else:
"form": data_form, # For clarification: nothing in this case
"geom_form": geom_form, pass
TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier), context = {
} "form": data_form,
context = BaseContext(request, context).context "geom_form": geom_form,
return render(request, self._TEMPLATE, context) 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_modal)
@method_decorator(login_required) @method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id")) @method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs) return super().dispatch(request, *args, **kwargs)

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 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.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -14,72 +14,67 @@ from ema.models import Ema
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.qrcode import QrCode from konova.utils.generators import generate_qr_code
from konova.views.report import AbstractPublicReportView
class EmaPublicReportView(AbstractPublicReportView): def report_view(request:HttpRequest, id: str):
_TEMPLATE = "ema/report/report.html" """ Renders the public report view
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: Args:
""" Renders the public report view request (HttpRequest): The incoming request
id (str): The id of the intervention
Args: Returns:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns: """
# Reuse the compensation report template since EMAs are structurally identical
""" template = "ema/report/report.html"
ema = get_object_or_404(Ema, id=id) ema = get_object_or_404(Ema, id=id)
tab_title = _("Report {}").format(ema.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not ema.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=ema,
)
parcels = ema.get_underlying_parcels()
qrcode = QrCode(
content=request.build_absolute_uri(reverse("ema:report", args=(id,))),
size=10
)
qrcode_lanis = QrCode(
content=ema.get_LANIS_link(),
size=7
)
# Order states by surface
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type")
actions = ema.actions.all().prefetch_related("action_type")
tab_title = _("Report {}").format(ema.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not ema.is_ready_for_publish():
template = "report/unavailable.html"
context = { context = {
"obj": ema,
"qrcode": {
"img": qrcode.get_img(),
"url": qrcode.get_content(),
},
"qrcode_lanis": {
"img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(),
},
"is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, 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", "geometry",
] ]
def get_actions(self, request):
DELETE_ACTION_IDENTIFIER = "delete_selected"
actions = super().get_actions(request)
if DELETE_ACTION_IDENTIFIER in actions:
del actions[DELETE_ACTION_IDENTIFIER]
return actions
class InterventionDocumentAdmin(AbstractDocumentAdmin): class InterventionDocumentAdmin(AbstractDocumentAdmin):
pass pass

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,42 +8,39 @@ Created on: 30.11.20
from django.urls import path from django.urls import path
from intervention.autocomplete.intervention import InterventionAutocomplete from intervention.autocomplete.intervention import InterventionAutocomplete
from intervention.views.check import InterventionCheckView from intervention.views.check import check_view
from intervention.views.compensation import RemoveCompensationFromInterventionView from intervention.views.compensation import remove_compensation_view
from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \ from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \
RemoveInterventionDeductionView RemoveInterventionDeductionView
from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \ from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \
RemoveInterventionDocumentView, EditInterventionDocumentView RemoveInterventionDocumentView, EditInterventionDocumentView
from intervention.views.intervention import IndexInterventionView, InterventionIdentifierGeneratorView, \ from intervention.views.intervention import index_view, new_view, new_id_view, detail_view, edit_view, remove_view
NewInterventionView, EditInterventionView
from intervention.views.remove import RemoveInterventionView
from intervention.views.detail import DetailInterventionView
from intervention.views.log import InterventionLogView from intervention.views.log import InterventionLogView
from intervention.views.record import InterventionRecordView from intervention.views.record import InterventionRecordView
from intervention.views.report import InterventionPublicReportView from intervention.views.report import report_view
from intervention.views.resubmission import InterventionResubmissionView from intervention.views.resubmission import InterventionResubmissionView
from intervention.views.revocation import NewInterventionRevocationView, GetInterventionRevocationView, \ from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \
EditInterventionRevocationView, RemoveInterventionRevocationView get_revocation_view
from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView
app_name = "intervention" app_name = "intervention"
urlpatterns = [ urlpatterns = [
path("", IndexInterventionView.as_view(), name="index"), path("", index_view, name="index"),
path('new/', NewInterventionView.as_view(), name='new'), path('new/', new_view, name='new'),
path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'), path('new/id', new_id_view, name='new-id'),
path('<id>', DetailInterventionView.as_view(), name='detail'), path('<id>', detail_view, name='detail'),
path('<id>/log', InterventionLogView.as_view(), name='log'), path('<id>/log', InterventionLogView.as_view(), name='log'),
path('<id>/edit', EditInterventionView.as_view(), name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', RemoveInterventionView.as_view(), name='remove'), path('<id>/remove', remove_view, name='remove'),
path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'), path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'),
path('<id>/share', InterventionShareFormView.as_view(), name='share-form'), path('<id>/share', InterventionShareFormView.as_view(), name='share-form'),
path('<id>/check', InterventionCheckView.as_view(), name='check'), path('<id>/check', check_view, name='check'),
path('<id>/record', InterventionRecordView.as_view(), name='record'), 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'), path('<id>/resub', InterventionResubmissionView.as_view(), name='resubmission-create'),
# Compensations # Compensations
path('<id>/compensation/<comp_id>/remove', RemoveCompensationFromInterventionView.as_view(), name='remove-compensation'), path('<id>/compensation/<comp_id>/remove', remove_compensation_view, name='remove-compensation'),
# Documents # Documents
path('<id>/document/new/', NewInterventionDocumentView.as_view(), name='new-doc'), path('<id>/document/new/', NewInterventionDocumentView.as_view(), name='new-doc'),
@@ -57,10 +54,10 @@ urlpatterns = [
path('<id>/deduction/<deduction_id>/remove', RemoveInterventionDeductionView.as_view(), name='remove-deduction'), path('<id>/deduction/<deduction_id>/remove', RemoveInterventionDeductionView.as_view(), name='remove-deduction'),
# Revocation routes # Revocation routes
path('<id>/revocation/new', NewInterventionRevocationView.as_view(), name='new-revocation'), path('<id>/revocation/new', new_revocation_view, name='new-revocation'),
path('<id>/revocation/<revocation_id>/edit', EditInterventionRevocationView.as_view(), name='edit-revocation'), path('<id>/revocation/<revocation_id>/edit', edit_revocation_view, name='edit-revocation'),
path('<id>/revocation/<revocation_id>/remove', RemoveInterventionRevocationView.as_view(), name='remove-revocation'), path('<id>/revocation/<revocation_id>/remove', remove_revocation_view, name='remove-revocation'),
path('revocation/<doc_id>', GetInterventionRevocationView.as_view(), name='get-doc-revocation'), path('revocation/<doc_id>', get_revocation_view, name='get-doc-revocation'),
# Autocomplete # Autocomplete
path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="autocomplete"), path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="autocomplete"),

View File

@@ -5,44 +5,35 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View
from intervention.forms.modals.check import CheckModalForm from intervention.forms.modals.check import CheckModalForm
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import registration_office_group_required, shared_access_required from konova.decorators import registration_office_group_required, shared_access_required
from konova.utils.message_templates import INTERVENTION_INVALID from konova.utils.message_templates import INTERVENTION_INVALID
class InterventionCheckView(LoginRequiredMixin, View):
def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: @login_required
""" Renders check form for an intervention @registration_office_group_required
@shared_access_required(Intervention, "id")
def check_view(request: HttpRequest, id: str):
""" Renders check form for an intervention
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): Intervention's id id (str): Intervention's id
Returns: Returns:
""" """
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
form = CheckModalForm(request.POST or None, instance=intervention, request=request) form = CheckModalForm(request.POST or None, instance=intervention, request=request)
return form.process_request( return form.process_request(
request, request,
msg_success=_("Check performed"), msg_success=_("Check performed"),
msg_error=INTERVENTION_INVALID msg_error=INTERVENTION_INVALID
) )
@method_decorator(registration_office_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, *args, **kwargs)
@method_decorator(registration_office_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, *args, **kwargs)

View File

@@ -5,50 +5,42 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest, Http404, HttpResponse from django.http import HttpRequest, Http404
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import shared_access_required from konova.decorators import shared_access_required, login_required_modal
from konova.forms.modals import RemoveModalForm from konova.forms.modals import RemoveModalForm
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
class RemoveCompensationFromInterventionView(LoginRequiredMixin, View): @login_required_modal
@login_required
@shared_access_required(Intervention, "id")
def remove_compensation_view(request: HttpRequest, id: str, comp_id: str):
""" Renders a modal view for removing the compensation
def __process_request(self, request: HttpRequest, id: str, comp_id: str, *args, **kwargs) -> HttpResponse: Args:
""" Renders a modal view for removing the compensation request (HttpRequest): The incoming request
id (str): The compensation's id
Args: Returns:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns: """
intervention = get_object_or_404(Intervention, id=id)
""" try:
intervention = get_object_or_404(Intervention, id=id) comp = intervention.compensations.get(
try: id=comp_id
comp = intervention.compensations.get(
id=comp_id
)
except ObjectDoesNotExist:
raise Http404("Unknown compensation")
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request=request,
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data",
) )
except ObjectDoesNotExist:
raise Http404("Unknown compensation")
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request=request,
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data",
)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request, id: str, comp_id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, comp_id, *args, **kwargs)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request, id: str, comp_id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, comp_id, *args, **kwargs)

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 import messages
from django.contrib.auth.decorators import login_required from django.contrib.auth.decorators import login_required
from django.contrib.auth.mixins import LoginRequiredMixin from django.http import JsonResponse, HttpRequest
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, render, redirect from django.shortcuts import get_object_or_404, render, redirect
from django.utils.decorators import method_decorator from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View
from intervention.forms.intervention import EditInterventionForm, NewInterventionForm from intervention.forms.intervention import EditInterventionForm, NewInterventionForm
from intervention.models import Intervention from intervention.models import Intervention
from intervention.tables import InterventionTable from intervention.tables import InterventionTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import default_group_required, shared_access_required from konova.decorators import default_group_required, shared_access_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, \ from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \
CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, \ CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
GEOMETRIES_IGNORED_TEMPLATE
from konova.views.identifier import AbstractIdentifierGeneratorView
from konova.views.index import AbstractIndexView
class IndexInterventionView(AbstractIndexView): @login_required
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: @any_group_check
""" def index_view(request: HttpRequest):
Renders the index view for Interventions """
Renders the index view for Interventions
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
Returns: Returns:
A rendered view A rendered view
""" """
# Filtering by user access is performed in table filter inside InterventionTableFilter class template = "generic_index.html"
interventions = Intervention.objects.filter(
deleted=None, # not deleted # Filtering by user access is performed in table filter inside InterventionTableFilter class
).select_related( interventions = Intervention.objects.filter(
"legal" deleted=None, # not deleted
).order_by( ).select_related(
"-modified__timestamp" "legal"
) ).order_by(
table = InterventionTable( "-modified__timestamp"
request=request, )
queryset=interventions table = InterventionTable(
) request=request,
context = { queryset=interventions
"table": table, )
TAB_TITLE_IDENTIFIER: _("Interventions - Overview"), context = {
} "table": table,
context = BaseContext(request, context).context TAB_TITLE_IDENTIFIER: _("Interventions - Overview"),
return render(request, self._TEMPLATE, context) }
context = BaseContext(request, context).context
return render(request, template, context)
class NewInterventionView(LoginRequiredMixin, View): @login_required
_TEMPLATE = "intervention/form/view.html" @default_group_required
def new_view(request: HttpRequest):
"""
Renders a view for a new intervention creation
@method_decorator(default_group_required) Args:
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse: request (HttpRequest): The incoming request
""" Returns:
Renders a view for a new intervention creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
data_form = NewInterventionForm()
geom_form = SimpleGeomForm(read_only=False)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New intervention"),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(default_group_required)
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
"""
Renders a view for a new intervention creation
Args:
request (HttpRequest): The incoming request
Returns:
"""
data_form = NewInterventionForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
"""
template = "intervention/form/view.html"
data_form = NewInterventionForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid(): if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None) generated_identifier = data_form.cleaned_data.get("identifier", None)
intervention = data_form.save(request.user, geom_form) intervention = data_form.save(request.user, geom_form)
@@ -110,100 +87,147 @@ class NewInterventionView(LoginRequiredMixin, View):
intervention.identifier intervention.identifier
) )
) )
messages.success(request, _("Intervention {} added").format(intervention.identifier)) messages.success(request, _("Intervention {} added").format(intervention.identifier))
if geom_form.has_geometry_simplified(): if geom_form.geometry_simplified:
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED GEOMETRY_SIMPLIFIED
) )
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("intervention:detail", id=intervention.id) return redirect("intervention:detail", id=intervention.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger", ) messages.error(request, FORM_INVALID, extra_tags="danger",)
context = { else:
"form": data_form, # For clarification: nothing in this case
"geom_form": geom_form, pass
TAB_TITLE_IDENTIFIER: _("New intervention"), 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): @login_required
_MODEL = Intervention @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): @login_required
_TEMPLATE = "intervention/form/view.html" @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) Args:
@method_decorator(shared_access_required(Intervention, "id")) request (HttpRequest): The incoming request
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
"""
Renders a view for editing interventions
Args: Returns:
request (HttpRequest): The incoming request
id (str): The intervention identifier
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 # Create forms, initialize with values from db/from POST request
intervention = get_object_or_404(Intervention, id=id) data_form = EditInterventionForm(request.POST or None, instance=intervention)
if intervention.is_recorded: geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
messages.info( if request.method == "POST":
request,
RECORDED_BLOCKS_EDIT
)
return redirect("intervention:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditInterventionForm(request.POST or None, instance=intervention)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier),
}
context = BaseContext(request, context).context
return render(request, self._TEMPLATE, context)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
"""
Process saved form content
Args:
request (HttpRequest): The incoming request
id (str): The intervention id
Returns:
HttpResponse:
"""
# Get object from db
intervention = get_object_or_404(Intervention, id=id)
if intervention.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("intervention:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditInterventionForm(request.POST or None, instance=intervention)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
if data_form.is_valid() and geom_form.is_valid(): if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user # The data form takes the geom form for processing, as well as the performing user
# Save the current state of recorded|checked to inform the user in case of a status reset due to editing # Save the current state of recorded|checked to inform the user in case of a status reset due to editing
@@ -212,22 +236,45 @@ class EditInterventionView(LoginRequiredMixin, View):
messages.success(request, _("Intervention {} edited").format(intervention.identifier)) messages.success(request, _("Intervention {} edited").format(intervention.identifier))
if intervention_is_checked: if intervention_is_checked:
messages.info(request, CHECK_STATE_RESET) messages.info(request, CHECK_STATE_RESET)
if geom_form.has_geometry_simplified(): if geom_form.geometry_simplified:
messages.info( messages.info(
request, request,
GEOMETRY_SIMPLIFIED GEOMETRY_SIMPLIFIED
) )
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("intervention:detail", id=intervention.id) return redirect("intervention:detail", id=intervention.id)
context = { else:
"form": data_form, messages.error(request, FORM_INVALID, extra_tags="danger",)
"geom_form": geom_form, else:
TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier), # For clarification: nothing in this case
} pass
context = BaseContext(request, context).context context = {
return render(request, self._TEMPLATE, 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 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.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.qrcode import QrCode from konova.utils.generators import generate_qr_code
from konova.views.report import AbstractPublicReportView
class InterventionPublicReportView(AbstractPublicReportView): @uuid_required
_TEMPLATE = "intervention/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: Args:
""" Renders the public report view request (HttpRequest): The incoming request
id (str): The id of the intervention
Args: Returns:
request (HttpRequest): The incoming request
id (str): The id of the intervention
Returns: """
template = "intervention/report/report.html"
""" intervention = get_object_or_404(Intervention, id=id)
intervention = get_object_or_404(Intervention, id=id)
tab_title = _("Report {}").format(intervention.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not intervention.is_ready_for_publish():
template = "report/unavailable.html"
context = {
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=intervention
)
parcels = intervention.get_underlying_parcels()
distinct_deductions = intervention.deductions.all().distinct(
"account"
)
qrcode = QrCode(
content=request.build_absolute_uri(reverse("intervention:report", args=(id,))),
size=10
)
qrcode_lanis = QrCode(
content=intervention.get_LANIS_link(),
size=7
)
tab_title = _("Report {}").format(intervention.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not intervention.is_ready_for_publish():
template = "report/unavailable.html"
context = { context = {
"obj": intervention,
"deductions": distinct_deductions,
"qrcode": {
"img": qrcode.get_img(),
"url": qrcode.get_content(),
},
"qrcode_lanis": {
"img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(),
},
"geom_form": geom_form,
"parcels": parcels,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
return render(request, 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

@@ -6,12 +6,10 @@ Created on: 19.08.22
""" """
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.http import HttpRequest, HttpResponse from django.http import HttpRequest
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse from django.urls import reverse
from django.utils.decorators import method_decorator
from django.views import View
from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \ from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \
RemoveRevocationModalForm RemoveRevocationModalForm
@@ -21,125 +19,100 @@ from konova.utils.documents import get_document
from konova.utils.message_templates import REVOCATION_ADDED, DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED from konova.utils.message_templates import REVOCATION_ADDED, DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED
class NewInterventionRevocationView(LoginRequiredMixin, View): @login_required
def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse: @default_group_required
""" Renders sharing form for an intervention @shared_access_required(Intervention, "id")
def new_revocation_view(request: HttpRequest, id: str):
""" Renders sharing form for an intervention
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): Intervention's id id (str): Intervention's id
Returns: Returns:
""" """
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, request=request)
request=request) return form.process_request(
return form.process_request( request,
msg_success=REVOCATION_ADDED,
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
)
@login_required
@default_group_required
def get_revocation_view(request: HttpRequest, doc_id: str):
""" Returns the revocation document as downloadable file
Wraps the generic document fetcher function from konova.utils.
Args:
request (HttpRequest): The incoming request
doc_id (str): The document id
Returns:
"""
doc = get_object_or_404(RevocationDocument, id=doc_id)
# File download only possible if related instance is shared with user
if not doc.instance.legal.intervention.users.filter(id=request.user.id):
messages.info(
request, request,
msg_success=REVOCATION_ADDED, DATA_UNSHARED
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
) )
return redirect("intervention:detail", id=doc.instance.id)
@method_decorator(default_group_required) return get_document(doc)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, *args, **kwargs)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, *args, **kwargs)
class GetInterventionRevocationView(LoginRequiredMixin, View): @login_required
@method_decorator(default_group_required) @default_group_required
def get(self, request: HttpRequest, doc_id: str, *args, **kwargs) -> HttpResponse: @shared_access_required(Intervention, "id")
""" Returns the revocation document as downloadable file def edit_revocation_view(request: HttpRequest, id: str, revocation_id: str):
""" Renders a edit view for a revocation
Wraps the generic document fetcher function from konova.utils. Args:
request (HttpRequest): The incoming request
id (str): The intervention's id as string
revocation_id (str): The revocation's id as string
Args: Returns:
request (HttpRequest): The incoming request
doc_id (str): The document id
Returns: """
intervention = get_object_or_404(Intervention, id=id)
revocation = get_object_or_404(Revocation, id=revocation_id)
""" form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, revocation=revocation, request=request)
doc = get_object_or_404(RevocationDocument, id=doc_id) return form.process_request(
# File download only possible if related instance is shared with user request,
if not doc.instance.legal.intervention.users.filter(id=request.user.id): REVOCATION_EDITED,
messages.info( redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
request, )
DATA_UNSHARED
)
return redirect("intervention:detail", id=doc.instance.id)
return get_document(doc)
class EditInterventionRevocationView(LoginRequiredMixin, View): @login_required_modal
def __process_request(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse: @login_required
""" Renders a edit view for a revocation @default_group_required
@shared_access_required(Intervention, "id")
def remove_revocation_view(request: HttpRequest, id: str, revocation_id: str):
""" Renders a remove view for a revocation
Args: Args:
request (HttpRequest): The incoming request request (HttpRequest): The incoming request
id (str): The intervention's id as string id (str): The intervention's id as string
revocation_id (str): The revocation's id as string revocation_id (str): The revocation's id as string
Returns: Returns:
""" """
intervention = get_object_or_404(Intervention, id=id) intervention = get_object_or_404(Intervention, id=id)
revocation = get_object_or_404(Revocation, id=revocation_id) revocation = get_object_or_404(Revocation, id=revocation_id)
form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation, request=request)
revocation=revocation, request=request) return form.process_request(
return form.process_request( request,
request, REVOCATION_REMOVED,
REVOCATION_EDITED, redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data" )
)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, revocation_id, *args, **kwargs)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, revocation_id, *args, **kwargs)
class RemoveInterventionRevocationView(LoginRequiredMixin, View):
def __process_request(self, request, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
""" Renders a remove view for a revocation
Args:
request (HttpRequest): The incoming request
id (str): The intervention's id as string
revocation_id (str): The revocation's id as string
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
revocation = get_object_or_404(Revocation, id=revocation_id)
form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation,
request=request)
return form.process_request(
request,
REVOCATION_REMOVED,
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def get(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, revocation_id, *args, **kwargs)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def post(self, request, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
return self.__process_request(request, id, revocation_id, *args, **kwargs)

View File

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

View File

@@ -11,7 +11,7 @@ from uuid import UUID
from bootstrap_modal_forms.mixins import is_ajax from bootstrap_modal_forms.mixins import is_ajax
from django.contrib import messages from django.contrib import messages
from django.core.exceptions import BadRequest from django.http import Http404
from django.shortcuts import redirect, get_object_or_404, render from django.shortcuts import redirect, get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -185,7 +185,7 @@ def uuid_required(function):
try: try:
uuid = UUID(uuid) uuid = UUID(uuid)
except ValueError: except ValueError:
raise BadRequest( raise Http404(
"Invalid UUID" "Invalid UUID"
) )
return function(request, *args, **kwargs) 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.record import RecordableTableFilterMixin
from konova.filters.mixins.self_created import SelfCreatedTableFilterMixin from konova.filters.mixins.self_created import SelfCreatedTableFilterMixin
from konova.filters.mixins.share import ShareableTableFilterMixin from konova.filters.mixins.share import ShareableTableFilterMixin
from konova.filters.mixins.user_log import UserLoggedTableFilterMixin
class AbstractTableFilter(django_filters.FilterSet): class AbstractTableFilter(django_filters.FilterSet):
@@ -41,8 +40,7 @@ class SelectionTableFilter(RegistrationOfficeTableFilterMixin,
class QueryTableFilter(KeywordTableFilterMixin, class QueryTableFilter(KeywordTableFilterMixin,
FileNumberTableFilterMixin, FileNumberTableFilterMixin,
GeoReferencedTableFilterMixin, GeoReferencedTableFilterMixin):
UserLoggedTableFilterMixin):
""" TableFilter holding different filter options for query related filtering """ TableFilter holding different filter options for query related filtering
""" """

View File

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

View File

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

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()

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