Compare commits
66 Commits
97fbe02742
..
1.19
| Author | SHA1 | Date | |
|---|---|---|---|
| 6f2b6c44d9 | |||
| 494e80a4ac | |||
| 1f6c81874b | |||
| 9ee016a8bb | |||
| 93d29982a6 | |||
| 59a1bdfb1c | |||
| 056a92b068 | |||
| c795d22e68 | |||
| 591527b048 | |||
| 3de6a905e1 | |||
| 9632e59456 | |||
| d65f60c07c | |||
| 3ae0dc0cc1 | |||
| b721e9c51c | |||
| d26e363f8b | |||
| 2df178f4e1 | |||
| 25471b6de7 | |||
| 9863807ad6 | |||
| 62e02d745f | |||
| 1a9de7f874 | |||
| 46b66eb95d | |||
| 3dd6c6ae8d | |||
| 64a4750187 | |||
| 6c6b3293fb | |||
| 09246616aa | |||
| f146aa983a | |||
| 60e9430542 | |||
| 970d0e79fa | |||
| 3f33de3626 | |||
| 9e5bb84ab4 | |||
| 4c372c1a04 | |||
| ee2c859a9e | |||
| 328f672ec0 | |||
| 88058d7caf | |||
| 0e6f8d5b55 | |||
| 047c9489fe | |||
| 38b81996ed | |||
| 3966521cd4 | |||
| e70a8b51d1 | |||
| 02dc0d0a59 | |||
| 0b84d418db | |||
| 6aad76866f | |||
| 1af807deae | |||
| a2bda8d230 | |||
| e4c459f92e | |||
| 2da6f1dc6f | |||
| 72914bab9d | |||
| fdf3adf5ae | |||
| 4c4d64cc3d | |||
| fbde03caec | |||
| 43eb598d3f | |||
| b7fac0ae03 | |||
| 447ba942b5 | |||
| 6df47f1615 | |||
| e25d549a97 | |||
| 5e65b8f4dc | |||
| 22cddb9902 | |||
| c986bd0b92 | |||
| 2c60d86177 | |||
| b7792ececc | |||
| 210f3fcafa | |||
| e7d67560f2 | |||
| b5e991fb95 | |||
| d3a555d406 | |||
| 9a374f50de | |||
| ce63dd30bc |
+2
-2
@@ -10,6 +10,6 @@ from analysis.views import *
|
||||
|
||||
app_name = "analysis"
|
||||
urlpatterns = [
|
||||
path("reports/", index_reports_view, name="reports"),
|
||||
path("reports/<id>", detail_report_view, name="report-detail"),
|
||||
path("reports/", ReportIndexView.as_view(), name="reports"),
|
||||
path("reports/<id>", ReportDetailView.as_view(), name="report-detail"),
|
||||
]
|
||||
+94
-35
@@ -1,8 +1,12 @@
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
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.utils.excel.excel import TempExcelFile
|
||||
@@ -42,57 +46,112 @@ def index_reports_view(request: HttpRequest):
|
||||
context = BaseContext(request, context).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:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The conservation_office KonovaCode id
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
|
||||
Returns:
|
||||
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
|
||||
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",
|
||||
"""
|
||||
template = "analysis/reports/index.html"
|
||||
form = TimespanReportForm(None)
|
||||
context = {
|
||||
"form": form
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, template, context)
|
||||
|
||||
@method_decorator(conservation_office_group_required)
|
||||
def post(self, request: HttpRequest) -> HttpResponse:
|
||||
|
||||
"""
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
|
||||
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
|
||||
)
|
||||
return redirect("analysis:reports")
|
||||
# Try to resolve the date parameters into Date objects -> redirect if this fails
|
||||
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
|
||||
format_param = request.GET.get("format", "html")
|
||||
report = TimespanReport(id, date_from, date_to)
|
||||
# Check whether the html default rendering is requested or an alternative
|
||||
format_param = request.GET.get("format", "html")
|
||||
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"
|
||||
context = {
|
||||
"office": cons_office,
|
||||
"office": office,
|
||||
"report": report,
|
||||
}
|
||||
context = BaseContext(request, context).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)
|
||||
response = HttpResponse(
|
||||
content=file.stream,
|
||||
content_type="application/ms-excel",
|
||||
)
|
||||
response['Content-Disposition'] = f'attachment; filename={cons_office.long_name}_{df}_{dt}.xlsx'
|
||||
response['Content-Disposition'] = f'attachment; filename={office.long_name}_{df}_{dt}.xlsx'
|
||||
return response
|
||||
else:
|
||||
raise NotImplementedError
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
# Generated by Django 6.0.5 on 2026-05-10 07:18
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('api', '0003_oauthtoken'),
|
||||
('user', '0010_user_sso_identifier'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name='ExternalIdentifier',
|
||||
fields=[
|
||||
('external_id', models.CharField(db_comment='Identifier from a source system', max_length=255, primary_key=True, serialize=False)),
|
||||
('internal_id', models.UUIDField(db_comment='Identifier in konova')),
|
||||
('created', models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, related_name='+', to='user.useractionlogentry')),
|
||||
],
|
||||
),
|
||||
]
|
||||
@@ -5,4 +5,5 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 21.01.22
|
||||
|
||||
"""
|
||||
from .token import *
|
||||
from .token import *
|
||||
from .external_identifier import *
|
||||
@@ -0,0 +1,51 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 10.05.26
|
||||
|
||||
"""
|
||||
from django.db import models
|
||||
|
||||
|
||||
class ExternalIdentifier(models.Model):
|
||||
""" Holds a lookup to match a given external identifier against the internal identifier in konova.
|
||||
|
||||
Relevant in cases of API transmitted entries, which are updates using external identifiers instead of
|
||||
the internal ones directly.
|
||||
|
||||
"""
|
||||
external_id = models.CharField(
|
||||
max_length=255,
|
||||
primary_key=True,
|
||||
db_comment="Identifier from a source system"
|
||||
)
|
||||
internal_id = models.UUIDField(
|
||||
db_comment="Identifier in konova"
|
||||
)
|
||||
created = models.ForeignKey(
|
||||
"user.UserActionLogEntry",
|
||||
on_delete=models.SET_NULL,
|
||||
null=True,
|
||||
blank=True,
|
||||
related_name='+'
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.external_id} -> {self.internal_id}"
|
||||
|
||||
@staticmethod
|
||||
def resolve_external_identifier(external_identifier: str):
|
||||
""" Returns a ExternalIdentifier object, if the given parameter could be resolved as an external identifier.
|
||||
|
||||
Args:
|
||||
external_identifier (str): An external identifier.
|
||||
|
||||
Returns:
|
||||
ExternalIdentifier | None
|
||||
"""
|
||||
if external_identifier:
|
||||
try:
|
||||
obj = ExternalIdentifier.objects.get(external_id=external_identifier)
|
||||
return obj
|
||||
except ExternalIdentifier.DoesNotExist:
|
||||
pass
|
||||
return None
|
||||
@@ -8,6 +8,7 @@
|
||||
"is_coherence_keeping": false,
|
||||
"is_pik": false,
|
||||
"intervention": "MUST_BE_SET_IN_TEST",
|
||||
"external_identifier": "LOREMIPSUM-123",
|
||||
"before_states": [
|
||||
],
|
||||
"after_states": [
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
],
|
||||
"properties": {
|
||||
"title": "Test_ecoaccount",
|
||||
"external_identifier": "LOREMIPSUM-1234",
|
||||
"deductable_surface": 10000.0,
|
||||
"is_pik": false,
|
||||
"responsible": {
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
],
|
||||
"properties": {
|
||||
"title": "Test_ema",
|
||||
"external_identifier": "LOREMIPSUM-1235",
|
||||
"is_pik": false,
|
||||
"responsible": {
|
||||
"conservation_office": null,
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
],
|
||||
"properties": {
|
||||
"title": "Test_intervention",
|
||||
"external_identifier": "LOREMIPSUM-1236",
|
||||
"responsible": {
|
||||
"registration_office": null,
|
||||
"registration_file_number": null,
|
||||
|
||||
@@ -9,6 +9,7 @@ import json
|
||||
|
||||
from django.urls import reverse
|
||||
|
||||
from api.models import ExternalIdentifier
|
||||
from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase
|
||||
|
||||
|
||||
@@ -42,7 +43,22 @@ class APIV1CreateTestCase(BaseAPIV1TestCase):
|
||||
response = self._run_create_request(url, post_body)
|
||||
self.assertEqual(response.status_code, 200, msg=response.content)
|
||||
content = json.loads(response.content)
|
||||
self.assertIsNotNone(content.get("id", None), msg=response.content)
|
||||
_id = content.get("id", None)
|
||||
self.assertIsNotNone(_id, msg=response.content)
|
||||
return _id
|
||||
|
||||
def _test_external_identifier_created(self, internal_id, external_id):
|
||||
""" Tests whether an external identifier has been created
|
||||
|
||||
Args:
|
||||
internal_id ():
|
||||
external_id ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
external_identifier = ExternalIdentifier.objects.get(internal_id=internal_id)
|
||||
self.assertEqual(external_identifier.external_id, external_id)
|
||||
|
||||
def test_create_intervention(self):
|
||||
""" Tests api creation
|
||||
@@ -54,7 +70,8 @@ class APIV1CreateTestCase(BaseAPIV1TestCase):
|
||||
json_file_path = "api/tests/v1/create/intervention_create_post_body.json"
|
||||
with open(json_file_path) as json_file:
|
||||
post_body = json.load(fp=json_file)
|
||||
self._test_create_object(url, post_body)
|
||||
internal_id = self._test_create_object(url, post_body)
|
||||
self._test_external_identifier_created(internal_id, post_body["properties"]["external_identifier"])
|
||||
|
||||
def test_create_compensation(self):
|
||||
""" Tests api creation
|
||||
@@ -71,13 +88,14 @@ class APIV1CreateTestCase(BaseAPIV1TestCase):
|
||||
# Expect this first request to fail, since user has no shared access on the intervention, we want to create
|
||||
# a compensation for
|
||||
response = self._run_create_request(url, post_body)
|
||||
self.assertEqual(response.status_code, 500, msg=response.content)
|
||||
self.assertEqual(response.status_code, 400, msg=response.content)
|
||||
content = json.loads(response.content)
|
||||
self.assertGreater(len(content.get("errors", [])), 0, msg=response.content)
|
||||
|
||||
# Add the user to the shared users of the intervention and try again! Now everything should work as expected.
|
||||
self.intervention.users.add(self.superuser)
|
||||
self._test_create_object(url, post_body)
|
||||
internal_id = self._test_create_object(url, post_body)
|
||||
self._test_external_identifier_created(internal_id, post_body["properties"]["external_identifier"])
|
||||
|
||||
def test_create_eco_account(self):
|
||||
""" Tests api creation
|
||||
@@ -89,7 +107,8 @@ class APIV1CreateTestCase(BaseAPIV1TestCase):
|
||||
json_file_path = "api/tests/v1/create/ecoaccount_create_post_body.json"
|
||||
with open(json_file_path) as json_file:
|
||||
post_body = json.load(fp=json_file)
|
||||
self._test_create_object(url, post_body)
|
||||
internal_id = self._test_create_object(url, post_body)
|
||||
self._test_external_identifier_created(internal_id, post_body["properties"]["external_identifier"])
|
||||
|
||||
def test_create_ema(self):
|
||||
""" Tests api creation
|
||||
@@ -101,7 +120,8 @@ class APIV1CreateTestCase(BaseAPIV1TestCase):
|
||||
json_file_path = "api/tests/v1/create/ema_create_post_body.json"
|
||||
with open(json_file_path) as json_file:
|
||||
post_body = json.load(fp=json_file)
|
||||
self._test_create_object(url, post_body)
|
||||
internal_id = self._test_create_object(url, post_body)
|
||||
self._test_external_identifier_created(internal_id, post_body["properties"]["external_identifier"])
|
||||
|
||||
def test_create_deduction(self):
|
||||
""" Tests api creation
|
||||
|
||||
@@ -31,9 +31,10 @@ class APIV1SharingTestCase(BaseAPIV1TestCase):
|
||||
def setUpTestData(cls):
|
||||
super().setUpTestData()
|
||||
|
||||
def _run_share_request(self, url, user_list: list):
|
||||
def _run_share_request(self, url, user_list: list, team_list: list):
|
||||
data = {
|
||||
"users": user_list
|
||||
"users": user_list,
|
||||
"teams": team_list
|
||||
}
|
||||
data = json.dumps(data)
|
||||
response = self.client.put(
|
||||
@@ -58,16 +59,22 @@ class APIV1SharingTestCase(BaseAPIV1TestCase):
|
||||
self.superuser.username,
|
||||
self.user.username,
|
||||
]
|
||||
team_list = [
|
||||
str(self.team.id),
|
||||
]
|
||||
|
||||
response = self._run_share_request(url, user_list)
|
||||
response = self._run_share_request(url, user_list, team_list)
|
||||
|
||||
# Must fail, since performing user has no access on requested object
|
||||
self.assertEqual(response.status_code, 500)
|
||||
self.assertTrue(len(json.loads(response.content.decode("utf-8")).get("errors", [])) > 0)
|
||||
|
||||
# Add performing user to shared access users and rerun the request
|
||||
# Add performing user to shared access users, switch from team id to team name and rerun the request
|
||||
obj.users.add(self.superuser)
|
||||
response = self._run_share_request(url, user_list)
|
||||
team_list = [
|
||||
self.team.name
|
||||
]
|
||||
response = self._run_share_request(url, user_list, team_list)
|
||||
|
||||
shared_users = obj.shared_users
|
||||
self.assertEqual(response.status_code, 200)
|
||||
@@ -84,14 +91,14 @@ class APIV1SharingTestCase(BaseAPIV1TestCase):
|
||||
share_url = reverse("api:v1:intervention-share", args=(self.intervention.id,))
|
||||
# Expect the first request to work properly
|
||||
self.intervention.users.add(self.superuser)
|
||||
response = self._run_share_request(share_url, [self.superuser.username])
|
||||
response = self._run_share_request(share_url, [self.superuser.username], [str(self.team.id)])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
|
||||
# Change the token
|
||||
self.header_data["HTTP_ksptoken"] = f"{self.superuser.api_token.token}__X"
|
||||
|
||||
# Expect the request to fail now
|
||||
response = self._run_share_request(share_url, [self.superuser.username])
|
||||
response = self._run_share_request(share_url, [self.superuser.username], [])
|
||||
self.assertEqual(response.status_code, 403)
|
||||
|
||||
def test_api_intervention_sharing(self):
|
||||
@@ -144,11 +151,11 @@ class APIV1SharingTestCase(BaseAPIV1TestCase):
|
||||
self.assertEqual(self.intervention.users.count(), 1)
|
||||
|
||||
# Try to add another user via API -> must work!
|
||||
response = self._run_share_request(share_url, [self.superuser.username, self.user.username])
|
||||
response = self._run_share_request(share_url, [self.superuser.username, self.user.username], [])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(self.intervention.users.count(), 2)
|
||||
|
||||
# Now try to remove the user again -> expect no changes at all to the shared user list
|
||||
response = self._run_share_request(share_url, [self.superuser.username])
|
||||
response = self._run_share_request(share_url, [self.superuser.username], [])
|
||||
self.assertEqual(response.status_code, 200)
|
||||
self.assertEqual(self.intervention.users.count(), 2)
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
],
|
||||
"properties": {
|
||||
"title": "TEST_compensation_CHANGED",
|
||||
"external_identifier": "LOREMIPSUM-123_CHANGED",
|
||||
"is_cef": true,
|
||||
"is_coherence_keeping": true,
|
||||
"is_pik": true,
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
],
|
||||
"properties": {
|
||||
"title": "TEST_account_CHANGED",
|
||||
"external_identifier": "LOREMIPSUM-1234_CHANGED",
|
||||
"deductable_surface": "100000.0",
|
||||
"is_pik": true,
|
||||
"responsible": {
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
],
|
||||
"properties": {
|
||||
"title": "TEST_EMA_CHANGED",
|
||||
"external_identifier": "LOREMIPSUM-1235_CHANGED",
|
||||
"responsible": {
|
||||
"conservation_office": null,
|
||||
"conservation_file_number": "TEST_CHANGED",
|
||||
|
||||
@@ -44,6 +44,7 @@
|
||||
],
|
||||
"properties": {
|
||||
"title": "Test_intervention_CHANGED",
|
||||
"external_identifier": "LOREMIPSUM-1236_CHANGED",
|
||||
"responsible": {
|
||||
"registration_office": null,
|
||||
"registration_file_number": "CHANGED",
|
||||
|
||||
@@ -7,11 +7,8 @@ Created on: 21.01.22
|
||||
"""
|
||||
from django.urls import path, include
|
||||
|
||||
from api.views.method_views import generate_new_token_view
|
||||
|
||||
app_name = "api"
|
||||
|
||||
urlpatterns = [
|
||||
path("v1/", include("api.urls.v1.urls", namespace="v1")),
|
||||
path("token/generate", generate_new_token_view, name="generate-new-token"),
|
||||
]
|
||||
@@ -10,9 +10,11 @@ from abc import abstractmethod
|
||||
|
||||
from django.contrib.gis import geos
|
||||
from django.contrib.gis.geos import GEOSGeometry
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.core.paginator import Paginator
|
||||
from django.db.models import Q
|
||||
|
||||
from api.models import ExternalIdentifier
|
||||
from konova.models import Geometry
|
||||
from konova.utils.message_templates import DATA_UNSHARED
|
||||
|
||||
@@ -76,6 +78,14 @@ class AbstractModelAPISerializer:
|
||||
del self.lookup["id"]
|
||||
else:
|
||||
# Return certain object
|
||||
## But first check, whether this is an external identifier ...
|
||||
try:
|
||||
## If we can find this _id on our ExternalIdentifier model, we need to map it on the internal id
|
||||
ext_id = ExternalIdentifier.objects.get(external_id=_id)
|
||||
_id = ext_id.internal_id
|
||||
except ObjectDoesNotExist:
|
||||
# If we did not find it, we assume that this is already an internal id. (Or it does not exist at all)
|
||||
pass
|
||||
self.lookup["id"] = _id
|
||||
|
||||
self.shared_lookup = Q(
|
||||
@@ -161,6 +171,14 @@ class AbstractModelAPISerializer:
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# First if there is an external identifier linked to an internal one, so we can continue with the internal
|
||||
try:
|
||||
ext_id = ExternalIdentifier.objects.get(external_id=id)
|
||||
id = ext_id.internal_id
|
||||
except ObjectDoesNotExist:
|
||||
# No external id found - let's hope the given id exists internally
|
||||
pass
|
||||
|
||||
obj = self.model.objects.get(
|
||||
id=id,
|
||||
deleted__isnull=True,
|
||||
|
||||
@@ -88,6 +88,11 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
|
||||
# Nothing to do here
|
||||
return obj
|
||||
|
||||
# Transform a potential external identifier into an internal one
|
||||
intervention_ext_id = self._get_external_identifier(intervention_id)
|
||||
if intervention_ext_id:
|
||||
intervention_id = intervention_ext_id.internal_id
|
||||
|
||||
intervention = Intervention.objects.get(
|
||||
id=intervention_id,
|
||||
)
|
||||
@@ -114,6 +119,10 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
|
||||
|
||||
# Fill in data to objects
|
||||
properties = json_model["properties"]
|
||||
|
||||
external_identifier = properties.get("external_identifier", None)
|
||||
self._check_external_identifier_on_entry_creation(external_identifier)
|
||||
|
||||
obj.identifier = obj.generate_new_identifier()
|
||||
obj.title = properties["title"]
|
||||
obj.is_cef = properties["is_cef"]
|
||||
@@ -129,6 +138,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
|
||||
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
|
||||
obj = self._set_deadlines(obj, properties["deadlines"])
|
||||
|
||||
self._set_external_identifier(obj.id, properties.get("external_identifier", None), obj.created)
|
||||
obj.log.add(obj.created)
|
||||
|
||||
celery_update_parcels.delay(obj.geometry.id)
|
||||
@@ -170,6 +180,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
|
||||
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
|
||||
obj = self._set_deadlines(obj, properties["deadlines"])
|
||||
|
||||
self._set_external_identifier(obj.id, properties.get("external_identifier", None), update_action)
|
||||
obj.log.add(update_action)
|
||||
|
||||
celery_update_parcels.delay(obj.geometry.id)
|
||||
|
||||
@@ -62,6 +62,14 @@ class DeductionAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
if surface <= 0:
|
||||
raise ValueError("Surface must be > 0 m²")
|
||||
|
||||
# Check if external identifiers need to be mapped onto internal ones
|
||||
acc_ext_id = self._get_external_identifier(acc_id)
|
||||
intervention_ext_id = self._get_external_identifier(intervention_id)
|
||||
if acc_ext_id:
|
||||
acc_id = acc_ext_id.internal_id
|
||||
if intervention_ext_id:
|
||||
intervention_id = intervention_ext_id.internal_id
|
||||
|
||||
acc = EcoAccount.objects.get(
|
||||
id=acc_id,
|
||||
deleted__isnull=True,
|
||||
|
||||
@@ -121,7 +121,9 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
obj = self._initialize_objects(json_model, user)
|
||||
|
||||
# Fill in data to objects
|
||||
properties = json_model["properties"]
|
||||
properties = json_model.get("properties", None)
|
||||
if not properties:
|
||||
raise AssertionError("No 'properties' found in payload!")
|
||||
obj.identifier = obj.generate_new_identifier()
|
||||
obj.title = properties["title"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
@@ -147,6 +149,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
|
||||
obj = self._set_deadlines(obj, properties["deadlines"])
|
||||
|
||||
self._set_external_identifier(obj.id, properties.get("external_identifier", None), obj.created)
|
||||
obj.log.add(obj.created)
|
||||
obj.users.add(user)
|
||||
|
||||
@@ -172,6 +175,10 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
|
||||
# Fill in data to objects
|
||||
properties = json_model["properties"]
|
||||
|
||||
external_identifier = properties.get("external_identifier", None)
|
||||
self._check_external_identifier_on_entry_creation(external_identifier)
|
||||
|
||||
obj.title = properties["title"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
obj.deductable_surface = float(properties["deductable_surface"])
|
||||
@@ -192,6 +199,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
|
||||
obj = self._set_deadlines(obj, properties["deadlines"])
|
||||
|
||||
self._set_external_identifier(obj.id, external_identifier, update_action)
|
||||
obj.log.add(update_action)
|
||||
|
||||
celery_update_parcels.delay(obj.geometry.id)
|
||||
|
||||
@@ -104,6 +104,10 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
|
||||
|
||||
# Fill in data to objects
|
||||
properties = json_model["properties"]
|
||||
|
||||
external_identifier = properties.get("external_identifier", None)
|
||||
self._check_external_identifier_on_entry_creation(external_identifier)
|
||||
|
||||
obj.identifier = obj.generate_new_identifier()
|
||||
obj.title = properties["title"]
|
||||
obj.is_pik = properties.get("is_pik", False)
|
||||
@@ -119,6 +123,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
|
||||
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
|
||||
obj = self._set_deadlines(obj, properties["deadlines"])
|
||||
|
||||
self._set_external_identifier(obj.id, external_identifier, obj.created)
|
||||
obj.log.add(obj.created)
|
||||
obj.users.add(user)
|
||||
|
||||
@@ -161,6 +166,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
|
||||
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
|
||||
obj = self._set_deadlines(obj, properties["deadlines"])
|
||||
|
||||
self._set_external_identifier(obj.id, properties.get("external_identifier", None), update_action)
|
||||
obj.log.add(update_action)
|
||||
|
||||
celery_update_parcels.delay(obj.geometry.id)
|
||||
|
||||
@@ -150,10 +150,14 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
|
||||
# Fill in data to objects
|
||||
properties = json_model["properties"]
|
||||
|
||||
external_identifier = properties.get("external_identifier", None)
|
||||
self._check_external_identifier_on_entry_creation(external_identifier)
|
||||
|
||||
obj.identifier = obj.generate_new_identifier()
|
||||
obj.title = properties["title"]
|
||||
self._set_responsibility(obj, properties["responsible"])
|
||||
self._set_legal(obj, properties["legal"])
|
||||
obj.title = properties.get("title", None)
|
||||
self._set_responsibility(obj, properties.get("responsible", None))
|
||||
self._set_legal(obj, properties.get("legal", None))
|
||||
|
||||
obj.responsible.handler.save()
|
||||
obj.responsible.save()
|
||||
@@ -161,6 +165,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
obj.legal.save()
|
||||
obj.save()
|
||||
|
||||
self._set_external_identifier(obj.id, external_identifier, obj.created)
|
||||
obj.users.add(user)
|
||||
obj.log.add(obj.created)
|
||||
|
||||
@@ -186,7 +191,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
|
||||
# Fill in data to objects
|
||||
properties = json_model["properties"]
|
||||
obj.title = properties["title"]
|
||||
obj.title = properties.get("title")
|
||||
self._set_responsibility(obj, properties.get("responsible", None))
|
||||
self._set_legal(obj, properties.get("legal", None))
|
||||
self._set_payments(obj, properties.get("payments", None))
|
||||
@@ -200,6 +205,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
|
||||
obj.save()
|
||||
|
||||
obj.mark_as_edited(user, edit_comment="API update")
|
||||
self._set_external_identifier(obj.id, properties.get("external_identifier", None), update_action)
|
||||
obj.send_data_to_egon()
|
||||
|
||||
celery_update_parcels.delay(obj.geometry.id)
|
||||
|
||||
@@ -12,6 +12,7 @@ from django.contrib.gis.geos import MultiPolygon
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db.models import QuerySet
|
||||
|
||||
from api.models import ExternalIdentifier
|
||||
from api.utils.serializer.serializer import AbstractModelAPISerializer
|
||||
from codelist.models import KonovaCode
|
||||
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \
|
||||
@@ -39,12 +40,20 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
|
||||
else:
|
||||
geom = MultiPolygon().geojson
|
||||
geo_json = json.loads(geom)
|
||||
ext_ids = list(
|
||||
ExternalIdentifier.objects.filter(
|
||||
internal_id=entry.id
|
||||
).values_list(
|
||||
"external_id", flat=True
|
||||
)
|
||||
)
|
||||
self.properties_data = {
|
||||
"id": entry.id,
|
||||
"identifier": entry.identifier,
|
||||
"title": entry.title,
|
||||
"created_on": self._created_on_to_json(entry),
|
||||
"modified_on": self._modified_on_to_json(entry),
|
||||
"external_identifiers": ext_ids,
|
||||
}
|
||||
self._extend_properties_data(entry)
|
||||
geo_json["properties"] = self.properties_data
|
||||
@@ -137,6 +146,57 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
|
||||
success = entry.deleted is not None
|
||||
return success
|
||||
|
||||
def _set_external_identifier(self, internal_identifier, external_identifier, log_entry):
|
||||
""" If an external identifier was provided in the payload, we set it
|
||||
in the database
|
||||
|
||||
Args:
|
||||
internal_identifier (BaseObject): The already processed konova object (EIV, KOM, ...)
|
||||
external_identifier (any): The external identifier taken from the payload
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
if external_identifier is None:
|
||||
return None
|
||||
|
||||
ext_id_obj = ExternalIdentifier.objects.get_or_create(
|
||||
internal_id=internal_identifier,
|
||||
external_id=external_identifier
|
||||
)[0]
|
||||
if not ext_id_obj.created:
|
||||
ext_id_obj.created = log_entry
|
||||
ext_id_obj.save()
|
||||
|
||||
return ext_id_obj
|
||||
|
||||
def _get_external_identifier(self, external_identifier):
|
||||
""" Checks whether a linkage based on an external identifier already exists and returns it if so.
|
||||
|
||||
Args:
|
||||
external_identifier (any): The external identifier according to payload
|
||||
|
||||
Returns:
|
||||
ExternalIdentifier | None
|
||||
"""
|
||||
return ExternalIdentifier.resolve_external_identifier(external_identifier)
|
||||
|
||||
def _check_external_identifier_on_entry_creation(self, external_identifier):
|
||||
""" Special check for POST processing:
|
||||
Checks whether an external identifier already exists on the database. This hints that
|
||||
the entry already has been created in the past. Instead of POST, the PUT method shall be used
|
||||
to avoid creating duplicates.
|
||||
|
||||
Args:
|
||||
external_identifier (any): The external identifier according to payload
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
persisted_external_identifier = self._get_external_identifier(external_identifier)
|
||||
if persisted_external_identifier:
|
||||
raise AssertionError(f"{external_identifier} has already been initially created! Use PUT for updates!")
|
||||
|
||||
|
||||
class DeductableAPISerializerV1Mixin:
|
||||
class Meta:
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
"""
|
||||
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
|
||||
@@ -6,7 +6,9 @@ Created on: 21.01.22
|
||||
|
||||
"""
|
||||
import json
|
||||
from json import JSONDecodeError
|
||||
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.http import JsonResponse, HttpRequest
|
||||
|
||||
from api.utils.serializer.v1.compensation import CompensationAPISerializerV1
|
||||
@@ -66,8 +68,12 @@ class AbstractAPIViewV1(AbstractAPIView):
|
||||
body = request.body.decode("utf-8")
|
||||
body = json.loads(body)
|
||||
created_id = self.serializer.create_model_from_json(body, self.user)
|
||||
except Exception as e:
|
||||
return self._return_error_response(e, 500)
|
||||
except (JSONDecodeError,
|
||||
AssertionError,
|
||||
ValueError,
|
||||
PermissionError,
|
||||
ObjectDoesNotExist) as e:
|
||||
return self._return_error_response(e, 400)
|
||||
return JsonResponse({"id": created_id})
|
||||
|
||||
def put(self, request: HttpRequest, id=None):
|
||||
|
||||
+69
-22
@@ -6,13 +6,14 @@ Created on: 21.01.22
|
||||
|
||||
"""
|
||||
import json
|
||||
import uuid
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.db.models import QuerySet, Q
|
||||
from django.http import JsonResponse, HttpRequest
|
||||
from django.views import View
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
from api.models import APIUserToken
|
||||
from api.models import APIUserToken, ExternalIdentifier
|
||||
from api.settings import KSP_TOKEN_HEADER_IDENTIFIER, KSP_USER_HEADER_IDENTIFIER
|
||||
from compensation.models import EcoAccount
|
||||
from ema.models import Ema
|
||||
@@ -81,9 +82,7 @@ class AbstractAPIView(View):
|
||||
Returns:
|
||||
|
||||
"""
|
||||
content = [error.__str__()]
|
||||
if hasattr(error, "messages"):
|
||||
content = error.messages
|
||||
content = [f"{error.__class__.__name__}: {str(error)}"]
|
||||
return JsonResponse(
|
||||
{
|
||||
"errors": content
|
||||
@@ -207,6 +206,10 @@ class AbstractModelShareAPIView(AbstractAPIView):
|
||||
|
||||
"""
|
||||
try:
|
||||
external_identifier = ExternalIdentifier.resolve_external_identifier(id)
|
||||
if external_identifier:
|
||||
id = external_identifier.internal_id
|
||||
|
||||
users = self._get_shared_users_of_object(id)
|
||||
teams = self._get_shared_teams_of_object(id)
|
||||
except Exception as e:
|
||||
@@ -239,6 +242,9 @@ class AbstractModelShareAPIView(AbstractAPIView):
|
||||
"""
|
||||
|
||||
try:
|
||||
external_identifier = ExternalIdentifier.resolve_external_identifier(id)
|
||||
if external_identifier:
|
||||
id = external_identifier.internal_id
|
||||
success = self._process_put_body(request.body, id)
|
||||
except Exception as e:
|
||||
return self._return_error_response(e)
|
||||
@@ -311,22 +317,36 @@ class AbstractModelShareAPIView(AbstractAPIView):
|
||||
raise ValueError("Shared user list must not be empty!")
|
||||
new_teams = content.get("teams", [])
|
||||
|
||||
self.__process_user_sharing(new_users, obj)
|
||||
self.__process_team_sharing(new_teams, obj)
|
||||
|
||||
return True
|
||||
|
||||
def __process_user_sharing(self, user_list: list, obj):
|
||||
""" Processes API sharing for user payload
|
||||
|
||||
Args:
|
||||
user_list (list): A list of users to share the obj with
|
||||
obj (BaseObject): The shareable object
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# Eliminate duplicates
|
||||
new_users = list(dict.fromkeys(new_users))
|
||||
new_teams = list(dict.fromkeys(new_teams))
|
||||
new_users = list(dict.fromkeys(user_list))
|
||||
|
||||
# Make sure each of these names exist as a user
|
||||
new_users_objs = []
|
||||
for user in new_users:
|
||||
new_users_objs.append(User.objects.get(username=user))
|
||||
|
||||
# Make sure each of these names exist as a user
|
||||
new_teams_objs = []
|
||||
for team_name in new_teams:
|
||||
new_teams_objs.append(Team.objects.get(name=team_name))
|
||||
try:
|
||||
user_obj = User.objects.get(username=user)
|
||||
except User.DoesNotExist:
|
||||
raise AssertionError(f"User with username {user} does not exist")
|
||||
new_users_objs.append(user_obj)
|
||||
|
||||
if self.user.is_default_group_only():
|
||||
# Default only users are not allowed to remove other users from having access. They can only add new ones!
|
||||
# So we need to keep the ones that already have access from being removed!
|
||||
new_users_to_be_added = User.objects.filter(
|
||||
username__in=new_users
|
||||
).exclude(
|
||||
@@ -334,17 +354,44 @@ class AbstractModelShareAPIView(AbstractAPIView):
|
||||
)
|
||||
new_users_objs = obj.shared_users.union(new_users_to_be_added)
|
||||
|
||||
new_teams_to_be_added = Team.objects.filter(
|
||||
name__in=new_teams
|
||||
).exclude(
|
||||
id__in=obj.shared_teams
|
||||
)
|
||||
new_teams_objs = obj.shared_teams.union(new_teams_to_be_added)
|
||||
|
||||
obj.share_with_user_list(new_users_objs)
|
||||
obj.share_with_team_list(new_teams_objs)
|
||||
return True
|
||||
|
||||
def __process_team_sharing(self, team_list: list, obj):
|
||||
""" Processes API sharing for team payload
|
||||
|
||||
Args:
|
||||
team_list (list): A list of teams to share the obj with
|
||||
obj (BaseObject): The shareable object
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# Eliminate duplicates
|
||||
new_teams = list(dict.fromkeys(team_list))
|
||||
|
||||
# Resolve team names or ids into objects
|
||||
new_team_ids = []
|
||||
for team in new_teams:
|
||||
try:
|
||||
uuid.UUID(team)
|
||||
try:
|
||||
new_team_ids.append(Team.objects.get(id=team).id)
|
||||
except Team.DoesNotExist:
|
||||
raise AssertionError(f"Team with id {team} does not exist!")
|
||||
except ValueError:
|
||||
# entry is a name and not a uuid -> try to resolve as name!
|
||||
try:
|
||||
new_team_ids.append(Team.objects.get(name=team).id)
|
||||
except Team.DoesNotExist:
|
||||
raise AssertionError(f"Team with name {team} does not exist!")
|
||||
|
||||
new_team_objs = Team.objects.filter(id__in=new_team_ids)
|
||||
if self.user.is_default_group_only():
|
||||
# Default only users are not allowed to remove other users from having access. They can only add new ones!
|
||||
# So we need to keep the ones that already have access from being removed!
|
||||
new_team_objs = obj.shared_teams.union(new_team_objs)
|
||||
|
||||
obj.share_with_team_list(new_team_objs)
|
||||
|
||||
class InterventionAPIShareView(AbstractModelShareAPIView):
|
||||
model = Intervention
|
||||
|
||||
@@ -45,6 +45,14 @@ class AbstractCompensationAdmin(BaseObjectAdmin):
|
||||
states = "\n".join(states)
|
||||
return states
|
||||
|
||||
def get_actions(self, request):
|
||||
DELETE_ACTION_IDENTIFIER = "delete_selected"
|
||||
actions = super().get_actions(request)
|
||||
|
||||
if DELETE_ACTION_IDENTIFIER in actions:
|
||||
del actions[DELETE_ACTION_IDENTIFIER]
|
||||
|
||||
return actions
|
||||
|
||||
class CompensationAdmin(AbstractCompensationAdmin):
|
||||
autocomplete_fields = [
|
||||
|
||||
@@ -168,17 +168,6 @@ class NewCompensationForm(AbstractCompensationForm,
|
||||
comp.log.add(action)
|
||||
return comp, action
|
||||
|
||||
def is_valid(self):
|
||||
valid = super().is_valid()
|
||||
intervention = self.cleaned_data.get("intervention", None)
|
||||
if intervention.is_recorded:
|
||||
valid &= False
|
||||
self.add_error(
|
||||
"intervention",
|
||||
_("This intervention is currently recorded. You cannot add further compensations as long as it is recorded.")
|
||||
)
|
||||
return valid
|
||||
|
||||
def save(self, user: User, geom_form: SimpleGeomForm):
|
||||
with transaction.atomic():
|
||||
comp, action = self.__create_comp(user)
|
||||
|
||||
@@ -15,6 +15,7 @@ from compensation.models import EcoAccount
|
||||
from intervention.models import Handler, Responsibility, Legal
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.forms.modals import RemoveModalForm
|
||||
from konova.settings import ETS_GROUP
|
||||
from konova.utils import validators
|
||||
from user.models import User, UserActionLogEntry
|
||||
|
||||
@@ -246,4 +247,13 @@ class RemoveEcoAccountModalForm(RemoveModalForm):
|
||||
"confirm",
|
||||
_("The account can not be removed, since there are still deductions.")
|
||||
)
|
||||
|
||||
# If there are deductions but the performing user is not part of an ETS group, we assume this poor
|
||||
# fella does not know what he/she does -> give a hint that they should contact someone in charge...
|
||||
user_is_ets_user = self.user.in_group(ETS_GROUP)
|
||||
if not user_is_ets_user:
|
||||
self.add_error(
|
||||
"confirm",
|
||||
_("Please contact the responsible conservation office to find a solution!")
|
||||
)
|
||||
return super_valid and not has_deductions
|
||||
|
||||
@@ -124,7 +124,7 @@ class CompensationTable(BaseTable, TableRenderMixin, TableOrderMixin):
|
||||
html += self.render_previously_checked_star(
|
||||
tooltip=tooltip,
|
||||
)
|
||||
return format_html(html)
|
||||
return format_html(html, None)
|
||||
|
||||
def render_r(self, value, record: Compensation):
|
||||
""" Renders the registered column for a compensation
|
||||
@@ -146,5 +146,5 @@ class CompensationTable(BaseTable, TableRenderMixin, TableOrderMixin):
|
||||
tooltip=tooltip,
|
||||
icn_filled=recorded,
|
||||
)
|
||||
return format_html(html)
|
||||
return format_html(html, None)
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin, TableOrderMixin):
|
||||
txt=value,
|
||||
new_tab=False,
|
||||
)
|
||||
return format_html(html)
|
||||
return format_html(html, None)
|
||||
|
||||
def render_av(self, value, record: EcoAccount):
|
||||
""" Renders the available column for an eco account
|
||||
@@ -113,7 +113,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin, TableOrderMixin):
|
||||
value_relative = 0
|
||||
html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative})
|
||||
html += f"{number_format(record.deductable_rest, decimal_pos=2)} m²"
|
||||
return format_html(html)
|
||||
return format_html(html, None)
|
||||
|
||||
def render_r(self, value, record: EcoAccount):
|
||||
""" Renders the recorded column for an eco account
|
||||
@@ -135,4 +135,4 @@ class EcoAccountTable(BaseTable, TableRenderMixin, TableOrderMixin):
|
||||
tooltip=tooltip,
|
||||
icn_filled=checked,
|
||||
)
|
||||
return format_html(html)
|
||||
return format_html(html, None)
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
</td>
|
||||
<td class="align-middle">
|
||||
{% if deduction.intervention.recorded %}
|
||||
<em title="{% trans 'Recorded on' %} {{obj.recorded.timestamp}} {% trans 'by' %} {{obj.recorded.user}}" class='fas fa-bookmark registered-bookmark'></em>
|
||||
<em title="{% trans 'Recorded on' %} {{deduction.intervention.recorded.timestamp}} {% trans 'by' %} {{deduction.intervention.recorded.user}}" class='fas fa-bookmark registered-bookmark'></em>
|
||||
{% else %}
|
||||
<em title="{% trans 'Not recorded yet' %}" class='far fa-bookmark'></em>
|
||||
{% endif %}
|
||||
|
||||
@@ -7,31 +7,32 @@ Created on: 24.08.21
|
||||
"""
|
||||
from django.urls import path
|
||||
|
||||
from compensation.views.compensation.detail import DetailCompensationView
|
||||
from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \
|
||||
GetCompensationDocumentView, RemoveCompensationDocumentView
|
||||
from compensation.views.compensation.remove import RemoveCompensationView
|
||||
from compensation.views.compensation.resubmission import CompensationResubmissionView
|
||||
from compensation.views.compensation.report import CompensationReportView
|
||||
from compensation.views.compensation.report import CompensationPublicReportView
|
||||
from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \
|
||||
RemoveCompensationDeadlineView
|
||||
from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \
|
||||
RemoveCompensationActionView
|
||||
from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \
|
||||
RemoveCompensationStateView
|
||||
from compensation.views.compensation.compensation import \
|
||||
remove_view, CompensationIndexView, CompensationIdentifierGeneratorView, CompensationDetailView, \
|
||||
NewCompensationFormView, EditCompensationFormView
|
||||
from compensation.views.compensation.compensation import IndexCompensationView, CompensationIdentifierGeneratorView, \
|
||||
EditCompensationView, NewCompensationView
|
||||
from compensation.views.compensation.log import CompensationLogView
|
||||
|
||||
urlpatterns = [
|
||||
# Main compensation
|
||||
path("", CompensationIndexView.as_view(), name="index"),
|
||||
path("", IndexCompensationView.as_view(), name="index"),
|
||||
path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'),
|
||||
path('new/<intervention_id>', NewCompensationFormView.as_view(), name='new'),
|
||||
path('new', NewCompensationFormView.as_view(), name='new'),
|
||||
path('<id>', CompensationDetailView.as_view(), name='detail'),
|
||||
path('new/<intervention_id>', NewCompensationView.as_view(), name='new'),
|
||||
path('new', NewCompensationView.as_view(), name='new'),
|
||||
path('<id>', DetailCompensationView.as_view(), name='detail'),
|
||||
path('<id>/log', CompensationLogView.as_view(), name='log'),
|
||||
path('<id>/edit', EditCompensationFormView.as_view(), name='edit'),
|
||||
path('<id>/remove', remove_view, name='remove'),
|
||||
path('<id>/edit', EditCompensationView.as_view(), name='edit'),
|
||||
path('<id>/remove', RemoveCompensationView.as_view(), name='remove'),
|
||||
|
||||
path('<id>/state/new', NewCompensationStateView.as_view(), name='new-state'),
|
||||
path('<id>/state/<state_id>/edit', EditCompensationStateView.as_view(), name='state-edit'),
|
||||
@@ -44,7 +45,7 @@ urlpatterns = [
|
||||
path('<id>/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"),
|
||||
path('<id>/deadline/<deadline_id>/edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'),
|
||||
path('<id>/deadline/<deadline_id>/remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'),
|
||||
path('<id>/report', CompensationReportView.as_view(), name='report'),
|
||||
path('<id>/report', CompensationPublicReportView.as_view(), name='report'),
|
||||
path('<id>/resub', CompensationResubmissionView.as_view(), name='resubmission-create'),
|
||||
|
||||
# Documents
|
||||
|
||||
@@ -8,12 +8,13 @@ Created on: 24.08.21
|
||||
from django.urls import path
|
||||
|
||||
from compensation.autocomplete.eco_account import EcoAccountAutocomplete
|
||||
from compensation.views.eco_account.eco_account import remove_view, \
|
||||
EcoAccountIndexView, EcoAccountIdentifierGeneratorView, EcoAccountDetailView, NewEcoAccountFormView, \
|
||||
EditEcoAccountFormView
|
||||
from compensation.views.eco_account.detail import DetailEcoAccountView
|
||||
from compensation.views.eco_account.eco_account import IndexEcoAccountView, EcoAccountIdentifierGeneratorView, \
|
||||
NewEcoAccountView, EditEcoAccountView
|
||||
from compensation.views.eco_account.log import EcoAccountLogView
|
||||
from compensation.views.eco_account.record import EcoAccountRecordView
|
||||
from compensation.views.eco_account.report import EcoAccountReportView
|
||||
from compensation.views.eco_account.remove import RemoveEcoAccountView
|
||||
from compensation.views.eco_account.report import EcoAccountPublicReportView
|
||||
from compensation.views.eco_account.resubmission import EcoAccountResubmissionView
|
||||
from compensation.views.eco_account.state import NewEcoAccountStateView, EditEcoAccountStateView, \
|
||||
RemoveEcoAccountStateView
|
||||
@@ -29,15 +30,15 @@ from compensation.views.eco_account.deduction import NewEcoAccountDeductionView,
|
||||
|
||||
app_name = "acc"
|
||||
urlpatterns = [
|
||||
path("", EcoAccountIndexView.as_view(), name="index"),
|
||||
path('new/', NewEcoAccountFormView.as_view(), name='new'),
|
||||
path("", IndexEcoAccountView.as_view(), name="index"),
|
||||
path('new/', NewEcoAccountView.as_view(), name='new'),
|
||||
path('new/id', EcoAccountIdentifierGeneratorView.as_view(), name='new-id'),
|
||||
path('<id>', EcoAccountDetailView.as_view(), name='detail'),
|
||||
path('<id>', DetailEcoAccountView.as_view(), name='detail'),
|
||||
path('<id>/log', EcoAccountLogView.as_view(), name='log'),
|
||||
path('<id>/record', EcoAccountRecordView.as_view(), name='record'),
|
||||
path('<id>/report', EcoAccountReportView.as_view(), name='report'),
|
||||
path('<id>/edit', EditEcoAccountFormView.as_view(), name='edit'),
|
||||
path('<id>/remove', remove_view, name='remove'),
|
||||
path('<id>/report', EcoAccountPublicReportView.as_view(), name='report'),
|
||||
path('<id>/edit', EditEcoAccountView.as_view(), name='edit'),
|
||||
path('<id>/remove', RemoveEcoAccountView.as_view(), name='remove'),
|
||||
path('<id>/resub', EcoAccountResubmissionView.as_view(), name='resubmission-create'),
|
||||
|
||||
path('<id>/state/new', NewEcoAccountStateView.as_view(), name='new-state'),
|
||||
|
||||
@@ -10,7 +10,7 @@ from compensation.views.payment import *
|
||||
|
||||
app_name = "pay"
|
||||
urlpatterns = [
|
||||
path('<id>/new', new_payment_view, name='new'),
|
||||
path('<id>/remove/<payment_id>', payment_remove_view, name='remove'),
|
||||
path('<id>/edit/<payment_id>', payment_edit_view, name='edit'),
|
||||
path('<id>/new', NewPaymentView.as_view(), name='new'),
|
||||
path('<id>/remove/<payment_id>', RemovePaymentView.as_view(), name='remove'),
|
||||
path('<id>/edit/<payment_id>', EditPaymentView.as_view(), name='edit'),
|
||||
]
|
||||
|
||||
@@ -6,183 +6,260 @@ Created on: 19.08.22
|
||||
|
||||
"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.http import HttpRequest, JsonResponse
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
|
||||
from compensation.forms.compensation import EditCompensationForm, NewCompensationForm
|
||||
from compensation.models import Compensation
|
||||
from compensation.tables.compensation import CompensationTable
|
||||
from intervention.models import Intervention
|
||||
from konova.decorators import shared_access_required, default_group_required, login_required_modal
|
||||
from konova.forms.modals import RemoveModalForm
|
||||
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
|
||||
RECORDED_BLOCKS_EDIT, PARAMS_INVALID
|
||||
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \
|
||||
BaseEditSpatialLocatedObjectFormView
|
||||
from konova.views.detail import BaseDetailView
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import shared_access_required, default_group_required
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, \
|
||||
IDENTIFIER_REPLACED, COMPENSATION_ADDED_TEMPLATE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
|
||||
from konova.views.identifier import AbstractIdentifierGeneratorView
|
||||
from konova.views.index import AbstractIndexView
|
||||
|
||||
|
||||
class CompensationIndexView(LoginRequiredMixin, BaseIndexView):
|
||||
_TAB_TITLE = _("Compensations - Overview")
|
||||
_INDEX_TABLE_CLS = CompensationTable
|
||||
class IndexCompensationView(AbstractIndexView):
|
||||
def get(self, request, *args, **kwargs) -> HttpResponse:
|
||||
"""
|
||||
Renders the index view for compensation
|
||||
|
||||
def _get_queryset(self):
|
||||
qs = Compensation.objects.filter(
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
|
||||
Returns:
|
||||
A rendered view
|
||||
"""
|
||||
compensations = Compensation.objects.filter(
|
||||
deleted=None, # only show those which are not deleted individually
|
||||
intervention__deleted=None, # and don't show the ones whose intervention has been deleted
|
||||
).order_by(
|
||||
"-modified__timestamp"
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class NewCompensationFormView(BaseNewSpatialLocatedObjectFormView):
|
||||
_FORM_CLS = NewCompensationForm
|
||||
_MODEL_CLS = Compensation
|
||||
_TEMPLATE = "compensation/form/view.html"
|
||||
_TAB_TITLE = _("New Compensation")
|
||||
_REDIRECT_URL = "compensation:detail"
|
||||
|
||||
def _user_has_shared_access(self, user, **kwargs):
|
||||
# On a new compensation make sure the intervention (if call came directly through an intervention's detail
|
||||
# view) is shared with the user
|
||||
intervention_id = kwargs.get("intervention_id", None)
|
||||
if not intervention_id:
|
||||
return True
|
||||
else:
|
||||
intervention = get_object_or_404(Intervention, id=intervention_id)
|
||||
return intervention.is_shared_with(user)
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
# User has to be an ets user
|
||||
return user.is_default_user()
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
# Make sure there is an existing intervention based on the given id
|
||||
# Compensations can not exist without an intervention
|
||||
intervention_id = kwargs.get("intervention_id", None)
|
||||
if intervention_id:
|
||||
try:
|
||||
intervention = Intervention.objects.get(id=intervention_id)
|
||||
if intervention.is_recorded:
|
||||
messages.info(
|
||||
request,
|
||||
RECORDED_BLOCKS_EDIT
|
||||
)
|
||||
return redirect("intervention:detail", id=intervention_id)
|
||||
except ObjectDoesNotExist:
|
||||
messages.error(request, PARAMS_INVALID, extra_tags="danger")
|
||||
return redirect("home")
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class EditCompensationFormView(BaseEditSpatialLocatedObjectFormView):
|
||||
_MODEL_CLS = Compensation
|
||||
_FORM_CLS = EditCompensationForm
|
||||
_TEMPLATE = "compensation/form/view.html"
|
||||
_REDIRECT_URL = "compensation:detail"
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
# User has to be a default user
|
||||
return user.is_default_user()
|
||||
|
||||
|
||||
class CompensationIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
|
||||
_MODEL_CLS = Compensation
|
||||
_REDIRECT_URL = "compensation:index"
|
||||
|
||||
|
||||
class CompensationDetailView(BaseDetailView):
|
||||
_MODEL_CLS = Compensation
|
||||
_TEMPLATE = "compensation/detail/compensation/view.html"
|
||||
|
||||
def _get_object(self, id: str):
|
||||
""" Returns the compensation
|
||||
|
||||
Args:
|
||||
id (str): The compensation's id
|
||||
|
||||
Returns:
|
||||
obj (Compensation): The compensation
|
||||
"""
|
||||
comp = get_object_or_404(
|
||||
Compensation.objects.select_related(
|
||||
"modified",
|
||||
"created",
|
||||
"geometry"
|
||||
),
|
||||
id=id,
|
||||
deleted=None,
|
||||
intervention__deleted=None,
|
||||
table = CompensationTable(
|
||||
request=request,
|
||||
queryset=compensations
|
||||
)
|
||||
return comp
|
||||
context = {
|
||||
"table": table,
|
||||
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
def _get_detail_context(self, obj: Compensation):
|
||||
""" Generate object specific detail context for view
|
||||
|
||||
class NewCompensationView(LoginRequiredMixin, View):
|
||||
_TEMPLATE = "compensation/form/view.html"
|
||||
|
||||
@method_decorator(default_group_required)
|
||||
@method_decorator(shared_access_required(Intervention, "intervention_id"))
|
||||
def get(self, request: HttpRequest, intervention_id: str = None, *args, **kwargs) -> HttpResponse:
|
||||
"""
|
||||
Renders a view for new compensation
|
||||
|
||||
A compensation creation may be called directly from the parent-intervention object. If so - we may take
|
||||
the intervention's id and directly link the compensation to it.
|
||||
|
||||
Args:
|
||||
obj (): The record
|
||||
request (HttpRequest): The incoming request
|
||||
intervention_id (str): The intervention identifier
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# Order states according to surface
|
||||
before_states = obj.before_states.all().prefetch_related("biotope_type").order_by("-surface")
|
||||
after_states = obj.after_states.all().prefetch_related("biotope_type").order_by("-surface")
|
||||
actions = obj.actions.all().prefetch_related("action_type")
|
||||
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)
|
||||
|
||||
# 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 = obj.get_surface_before_states()
|
||||
sum_after_states = obj.get_surface_after_states()
|
||||
diff_states = abs(sum_before_states - sum_after_states)
|
||||
|
||||
last_checked = obj.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
|
||||
)
|
||||
data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
|
||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
||||
|
||||
context = {
|
||||
"last_checked": last_checked,
|
||||
"last_checked_tooltip": last_checked_tooltip,
|
||||
"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,
|
||||
"has_finished_deadlines": obj.get_finished_deadlines().exists(),
|
||||
"form": data_form,
|
||||
"geom_form": geom_form,
|
||||
TAB_TITLE_IDENTIFIER: _("New compensation"),
|
||||
}
|
||||
return context
|
||||
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)
|
||||
|
||||
if data_form.is_valid() and geom_form.is_valid():
|
||||
generated_identifier = data_form.cleaned_data.get("identifier", None)
|
||||
comp = data_form.save(request.user, geom_form)
|
||||
if generated_identifier != comp.identifier:
|
||||
messages.info(
|
||||
request,
|
||||
IDENTIFIER_REPLACED.format(
|
||||
generated_identifier,
|
||||
comp.identifier
|
||||
)
|
||||
)
|
||||
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
|
||||
if geom_form.has_geometry_simplified():
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRY_SIMPLIFIED
|
||||
)
|
||||
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||
if num_ignored_geometries > 0:
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||
)
|
||||
return redirect("compensation:detail", id=comp.id)
|
||||
else:
|
||||
messages.error(request, FORM_INVALID, extra_tags="danger", )
|
||||
|
||||
context = {
|
||||
"form": data_form,
|
||||
"geom_form": geom_form,
|
||||
TAB_TITLE_IDENTIFIER: _("New compensation"),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
|
||||
return render(request, self._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
|
||||
class CompensationIdentifierGeneratorView(AbstractIdentifierGeneratorView):
|
||||
_MODEL = Compensation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The compensation's id
|
||||
|
||||
Returns:
|
||||
class EditCompensationView(LoginRequiredMixin, View):
|
||||
_TEMPLATE = "compensation/form/view.html"
|
||||
|
||||
"""
|
||||
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"),
|
||||
)
|
||||
@method_decorator(default_group_required)
|
||||
@method_decorator(shared_access_required(Compensation, "id"))
|
||||
def get(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)
|
||||
|
||||
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():
|
||||
# Preserve state of intervention checked to determine whether the user must be informed or not
|
||||
# about a change of the check state
|
||||
intervention_is_checked = comp.intervention.checked is not None
|
||||
# The data form takes the geom form for processing, as well as the performing user
|
||||
comp = data_form.save(request.user, geom_form)
|
||||
if intervention_is_checked:
|
||||
messages.info(request, CHECK_STATE_RESET)
|
||||
messages.success(request, _("Compensation {} edited").format(comp.identifier))
|
||||
if geom_form.has_geometry_simplified():
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRY_SIMPLIFIED
|
||||
)
|
||||
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||
if num_ignored_geometries > 0:
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||
)
|
||||
return redirect("compensation:detail", id=comp.id)
|
||||
else:
|
||||
messages.error(request, FORM_INVALID, extra_tags="danger", )
|
||||
|
||||
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)
|
||||
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from django.contrib import messages
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
|
||||
from compensation.models import Compensation
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.settings import ETS_GROUP, ZB_GROUP, DEFAULT_GROUP
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE, DATA_CHECKED_PREVIOUSLY_TEMPLATE
|
||||
from konova.views.detail import AbstractDetailView
|
||||
|
||||
|
||||
class DetailCompensationView(AbstractDetailView):
|
||||
_TEMPLATE = "compensation/detail/compensation/view.html"
|
||||
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
|
||||
""" Renders a detail view for a compensation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The compensation's id
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
comp = get_object_or_404(
|
||||
Compensation.objects.select_related(
|
||||
"modified",
|
||||
"created",
|
||||
"geometry"
|
||||
),
|
||||
id=id,
|
||||
deleted=None,
|
||||
intervention__deleted=None,
|
||||
)
|
||||
geom_form = SimpleGeomForm(instance=comp)
|
||||
parcels = comp.get_underlying_parcels()
|
||||
_user = request.user
|
||||
is_data_shared = comp.intervention.is_shared_with(_user)
|
||||
|
||||
# Order states according to surface
|
||||
before_states = comp.before_states.all().prefetch_related("biotope_type").order_by("-surface")
|
||||
after_states = comp.after_states.all().prefetch_related("biotope_type").order_by("-surface")
|
||||
actions = comp.actions.all().prefetch_related("action_type")
|
||||
|
||||
# Precalculate logical errors between before- and after-states
|
||||
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
|
||||
sum_before_states = comp.get_surface_before_states()
|
||||
sum_after_states = comp.get_surface_after_states()
|
||||
diff_states = abs(sum_before_states - sum_after_states)
|
||||
|
||||
request = comp.set_status_messages(request)
|
||||
|
||||
last_checked = comp.intervention.get_last_checked_action()
|
||||
last_checked_tooltip = ""
|
||||
if last_checked:
|
||||
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
|
||||
last_checked.get_timestamp_str_formatted(),
|
||||
last_checked.user
|
||||
)
|
||||
|
||||
requesting_user_is_only_shared_user = comp.is_only_shared_with(_user)
|
||||
if requesting_user_is_only_shared_user:
|
||||
messages.info(
|
||||
request,
|
||||
DO_NOT_FORGET_TO_SHARE
|
||||
)
|
||||
|
||||
context = {
|
||||
"obj": comp,
|
||||
"last_checked": last_checked,
|
||||
"last_checked_tooltip": last_checked_tooltip,
|
||||
"geom_form": geom_form,
|
||||
"parcels": parcels,
|
||||
"is_entry_shared": is_data_shared,
|
||||
"actions": actions,
|
||||
"before_states": before_states,
|
||||
"after_states": after_states,
|
||||
"sum_before_states": sum_before_states,
|
||||
"sum_after_states": sum_after_states,
|
||||
"diff_states": diff_states,
|
||||
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
||||
"is_zb_member": _user.in_group(ZB_GROUP),
|
||||
"is_ets_member": _user.in_group(ETS_GROUP),
|
||||
"LANIS_LINK": comp.get_LANIS_link(),
|
||||
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}",
|
||||
"has_finished_deadlines": comp.get_finished_deadlines().exists(),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from compensation.models import Compensation
|
||||
from konova.decorators import shared_access_required
|
||||
from konova.views.remove import AbstractRemoveView
|
||||
|
||||
|
||||
class RemoveCompensationView(AbstractRemoveView):
|
||||
_MODEL = Compensation
|
||||
_REDIRECT_URL = "compensation:index"
|
||||
|
||||
@method_decorator(shared_access_required(Compensation, "id"))
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
return super().get(request, *args, **kwargs)
|
||||
@@ -5,48 +5,81 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 19.08.22
|
||||
|
||||
"""
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from compensation.models import Compensation
|
||||
from konova.sub_settings.django_settings import BASE_URL
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.qrcode import QrCode
|
||||
from konova.views.report import BaseReportView
|
||||
from konova.views.report import AbstractPublicReportView
|
||||
|
||||
|
||||
class BaseCompensationReportView(BaseReportView):
|
||||
def _get_compensation_report_context(self, obj):
|
||||
# Order states by surface
|
||||
before_states = obj.before_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||
after_states = obj.after_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||
actions = obj.actions.all().prefetch_related("action_type")
|
||||
|
||||
return {
|
||||
"before_states": before_states,
|
||||
"after_states": after_states,
|
||||
"actions": actions,
|
||||
}
|
||||
|
||||
|
||||
class CompensationReportView(BaseCompensationReportView):
|
||||
_MODEL = Compensation
|
||||
class CompensationPublicReportView(AbstractPublicReportView):
|
||||
_TEMPLATE = "compensation/report/compensation/report.html"
|
||||
|
||||
def _get_report_context(self, obj):
|
||||
report_url = BASE_URL + reverse("compensation:report", args=(obj.id,))
|
||||
qrcode_report = QrCode(report_url, 10)
|
||||
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" Renders the public report view
|
||||
|
||||
report_context = {
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The id of the intervention
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
comp = get_object_or_404(Compensation, id=id)
|
||||
tab_title = _("Report {}").format(comp.identifier)
|
||||
# If intervention is not recorded (yet or currently) we need to render another template without any data
|
||||
if not comp.is_ready_for_publish():
|
||||
template = "report/unavailable.html"
|
||||
context = {
|
||||
TAB_TITLE_IDENTIFIER: tab_title,
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, template, context)
|
||||
|
||||
# Prepare data for map viewer
|
||||
geom_form = SimpleGeomForm(
|
||||
instance=comp
|
||||
)
|
||||
parcels = comp.get_underlying_parcels()
|
||||
|
||||
qrcode = QrCode(
|
||||
content=request.build_absolute_uri(reverse("compensation:report", args=(id,))),
|
||||
size=10
|
||||
)
|
||||
qrcode_lanis = QrCode(
|
||||
content=comp.get_LANIS_link(),
|
||||
size=7
|
||||
)
|
||||
|
||||
# Order states by surface
|
||||
before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||
after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||
actions = comp.actions.all().prefetch_related("action_type")
|
||||
|
||||
context = {
|
||||
"obj": comp,
|
||||
"qrcode": {
|
||||
"img": qrcode_report.get_img(),
|
||||
"url": qrcode_report.get_content(),
|
||||
"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,
|
||||
}
|
||||
report_context.update(self._get_compensation_report_context(obj))
|
||||
return report_context
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
@@ -5,29 +5,54 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 19.08.22
|
||||
|
||||
"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import Http404
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from compensation.models import EcoAccount
|
||||
from konova.decorators import default_group_required, login_required_modal
|
||||
from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView
|
||||
|
||||
_ECO_ACCOUNT_DETAIl_URL_NAME = "compensation:acc:detail"
|
||||
|
||||
class NewEcoAccountDeductionView(LoginRequiredMixin, AbstractNewDeductionView):
|
||||
_MODEL_CLS = EcoAccount
|
||||
_REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME
|
||||
class NewEcoAccountDeductionView(AbstractNewDeductionView):
|
||||
model = EcoAccount
|
||||
redirect_url = "compensation:acc:detail"
|
||||
|
||||
@method_decorator(login_required_modal)
|
||||
@method_decorator(login_required)
|
||||
@method_decorator(default_group_required)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def _custom_check(self, obj):
|
||||
# New deductions can only be created if the eco account has been recorded
|
||||
if not obj.recorded:
|
||||
raise Http404()
|
||||
|
||||
|
||||
class EditEcoAccountDeductionView(LoginRequiredMixin, AbstractEditDeductionView):
|
||||
_MODEL_CLS = EcoAccount
|
||||
_REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME
|
||||
class EditEcoAccountDeductionView(AbstractEditDeductionView):
|
||||
def _custom_check(self, obj):
|
||||
pass
|
||||
|
||||
model = EcoAccount
|
||||
redirect_url = "compensation:acc:detail"
|
||||
|
||||
@method_decorator(login_required_modal)
|
||||
@method_decorator(login_required)
|
||||
@method_decorator(default_group_required)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RemoveEcoAccountDeductionView(LoginRequiredMixin, AbstractRemoveDeductionView):
|
||||
_MODEL_CLS = EcoAccount
|
||||
_REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME
|
||||
class RemoveEcoAccountDeductionView(AbstractRemoveDeductionView):
|
||||
def _custom_check(self, obj):
|
||||
pass
|
||||
|
||||
model = EcoAccount
|
||||
redirect_url = "compensation:acc:detail"
|
||||
|
||||
@method_decorator(login_required_modal)
|
||||
@method_decorator(login_required)
|
||||
@method_decorator(default_group_required)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from django.contrib import messages
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import render, get_object_or_404
|
||||
|
||||
from compensation.models import EcoAccount
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.settings import ETS_GROUP, ZB_GROUP, DEFAULT_GROUP
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE
|
||||
from konova.views.detail import AbstractDetailView
|
||||
|
||||
|
||||
class DetailEcoAccountView(AbstractDetailView):
|
||||
_TEMPLATE = "compensation/detail/eco_account/view.html"
|
||||
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" Renders a detail view for a compensation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The compensation's id
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
acc = get_object_or_404(
|
||||
EcoAccount.objects.prefetch_related(
|
||||
"deadlines",
|
||||
).select_related(
|
||||
'geometry',
|
||||
'responsible',
|
||||
),
|
||||
id=id,
|
||||
deleted=None,
|
||||
)
|
||||
geom_form = SimpleGeomForm(instance=acc)
|
||||
parcels = acc.get_underlying_parcels()
|
||||
_user = request.user
|
||||
is_data_shared = acc.is_shared_with(_user)
|
||||
|
||||
# Order states according to surface
|
||||
before_states = acc.before_states.order_by("-surface")
|
||||
after_states = acc.after_states.order_by("-surface")
|
||||
|
||||
# Precalculate logical errors between before- and after-states
|
||||
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
|
||||
sum_before_states = acc.get_surface_before_states()
|
||||
sum_after_states = acc.get_surface_after_states()
|
||||
diff_states = abs(sum_before_states - sum_after_states)
|
||||
# Calculate rest of available surface for deductions
|
||||
available_total = acc.deductable_rest
|
||||
available_relative = acc.get_deductable_rest_relative()
|
||||
|
||||
# Prefetch related data to decrease the amount of db connections
|
||||
deductions = acc.deductions.filter(
|
||||
intervention__deleted=None,
|
||||
)
|
||||
actions = acc.actions.all()
|
||||
|
||||
request = acc.set_status_messages(request)
|
||||
|
||||
requesting_user_is_only_shared_user = acc.is_only_shared_with(_user)
|
||||
if requesting_user_is_only_shared_user:
|
||||
messages.info(
|
||||
request,
|
||||
DO_NOT_FORGET_TO_SHARE
|
||||
)
|
||||
|
||||
context = {
|
||||
"obj": acc,
|
||||
"geom_form": geom_form,
|
||||
"parcels": parcels,
|
||||
"is_entry_shared": is_data_shared,
|
||||
"before_states": before_states,
|
||||
"after_states": after_states,
|
||||
"sum_before_states": sum_before_states,
|
||||
"sum_after_states": sum_after_states,
|
||||
"diff_states": diff_states,
|
||||
"available": available_relative,
|
||||
"available_total": available_total,
|
||||
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
||||
"is_zb_member": _user.in_group(ZB_GROUP),
|
||||
"is_ets_member": _user.in_group(ETS_GROUP),
|
||||
"LANIS_LINK": acc.get_LANIS_link(),
|
||||
"deductions": deductions,
|
||||
"actions": actions,
|
||||
TAB_TITLE_IDENTIFIER: f"{acc.identifier} - {acc.title}",
|
||||
"has_finished_deadlines": acc.get_finished_deadlines().exists(),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
@@ -6,80 +6,94 @@ Created on: 19.08.22
|
||||
|
||||
"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
|
||||
from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm, RemoveEcoAccountModalForm
|
||||
from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm
|
||||
from compensation.models import EcoAccount
|
||||
from compensation.tables.eco_account import EcoAccountTable
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import shared_access_required, default_group_required, login_required_modal
|
||||
from konova.decorators import shared_access_required, default_group_required
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.settings import ETS_GROUP
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
|
||||
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, FORM_INVALID, \
|
||||
IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
|
||||
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \
|
||||
BaseEditSpatialLocatedObjectFormView
|
||||
from konova.views.detail import BaseDetailView
|
||||
from konova.views.identifier import AbstractIdentifierGeneratorView
|
||||
from konova.views.index import AbstractIndexView
|
||||
|
||||
|
||||
class EcoAccountIndexView(LoginRequiredMixin, BaseIndexView):
|
||||
_INDEX_TABLE_CLS = EcoAccountTable
|
||||
_TAB_TITLE = _("Eco-account - Overview")
|
||||
class IndexEcoAccountView(AbstractIndexView):
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""
|
||||
Renders the index view for eco accounts
|
||||
|
||||
def _get_queryset(self):
|
||||
qs = EcoAccount.objects.filter(
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
|
||||
Returns:
|
||||
A rendered view
|
||||
"""
|
||||
eco_accounts = EcoAccount.objects.filter(
|
||||
deleted=None,
|
||||
).order_by(
|
||||
"-modified__timestamp"
|
||||
)
|
||||
return qs
|
||||
table = EcoAccountTable(
|
||||
request=request,
|
||||
queryset=eco_accounts
|
||||
)
|
||||
context = {
|
||||
"table": table,
|
||||
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
|
||||
class NewEcoAccountFormView(BaseNewSpatialLocatedObjectFormView):
|
||||
_FORM_CLS = NewEcoAccountForm
|
||||
_MODEL_CLS = EcoAccount
|
||||
class NewEcoAccountView(LoginRequiredMixin, View):
|
||||
_TEMPLATE = "compensation/form/view.html"
|
||||
_TAB_TITLE = _("New Eco-Account")
|
||||
_REDIRECT_URL = "compensation:acc:detail"
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
# User has to be a default user
|
||||
return user.is_default_user()
|
||||
@method_decorator(default_group_required)
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""
|
||||
Renders a view for a new eco account creation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
|
||||
class EditEcoAccountFormView(BaseEditSpatialLocatedObjectFormView):
|
||||
_FORM_CLS = EditEcoAccountForm
|
||||
_MODEL_CLS = EcoAccount
|
||||
_TEMPLATE = "compensation/form/view.html"
|
||||
_REDIRECT_URL = "compensation:acc:detail"
|
||||
Returns:
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
# User has to be a default user
|
||||
return user.is_default_user()
|
||||
"""
|
||||
data_form = NewEcoAccountForm(request.POST or None)
|
||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
||||
|
||||
context = {
|
||||
"form": data_form,
|
||||
"geom_form": geom_form,
|
||||
TAB_TITLE_IDENTIFIER: _("New Eco-Account"),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
|
||||
@login_required
|
||||
@default_group_required
|
||||
def new_view(request: HttpRequest):
|
||||
"""
|
||||
Renders a view for a new eco account creation
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
@method_decorator(default_group_required)
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
|
||||
Returns:
|
||||
"""
|
||||
Renders a view for a new eco account creation
|
||||
|
||||
"""
|
||||
template = "compensation/form/view.html"
|
||||
data_form = NewEcoAccountForm(request.POST or None)
|
||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
||||
if request.method == "POST":
|
||||
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():
|
||||
generated_identifier = data_form.cleaned_data.get("identifier", None)
|
||||
acc = data_form.save(request.user, geom_form)
|
||||
@@ -97,61 +111,92 @@ def new_view(request: HttpRequest):
|
||||
request,
|
||||
GEOMETRY_SIMPLIFIED
|
||||
)
|
||||
|
||||
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||
if num_ignored_geometries > 0:
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||
)
|
||||
|
||||
return redirect("compensation:acc:detail", id=acc.id)
|
||||
else:
|
||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
||||
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)
|
||||
messages.error(request, FORM_INVALID, extra_tags="danger", )
|
||||
|
||||
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)
|
||||
|
||||
class EcoAccountIdentifierGeneratorView(AbstractIdentifierGeneratorView):
|
||||
_MODEL = EcoAccount
|
||||
|
||||
|
||||
class EcoAccountIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
|
||||
_MODEL_CLS = EcoAccount
|
||||
_REDIRECT_URL = "compensation:acc:index"
|
||||
class EditEcoAccountView(LoginRequiredMixin, View):
|
||||
_TEMPLATE = "compensation/form/view.html"
|
||||
|
||||
@method_decorator(default_group_required)
|
||||
@method_decorator(shared_access_required(EcoAccount, "id"))
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
|
||||
@login_required
|
||||
@default_group_required
|
||||
@shared_access_required(EcoAccount, "id")
|
||||
def edit_view(request: HttpRequest, id: str):
|
||||
"""
|
||||
Renders a view for editing compensations
|
||||
"""
|
||||
Renders a view for editing compensations
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
|
||||
Returns:
|
||||
Returns:
|
||||
|
||||
"""
|
||||
template = "compensation/form/view.html"
|
||||
# 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)
|
||||
"""
|
||||
# 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)
|
||||
|
||||
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()
|
||||
geom_form_valid = geom_form.is_valid()
|
||||
if data_form_valid and geom_form_valid:
|
||||
@@ -163,125 +208,21 @@ def edit_view(request: HttpRequest, id: str):
|
||||
request,
|
||||
GEOMETRY_SIMPLIFIED
|
||||
)
|
||||
|
||||
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||
if num_ignored_geometries > 0:
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||
)
|
||||
|
||||
return redirect("compensation:acc:detail", id=acc.id)
|
||||
else:
|
||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
||||
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)
|
||||
|
||||
|
||||
class EcoAccountDetailView(BaseDetailView):
|
||||
_MODEL_CLS = EcoAccount
|
||||
_TEMPLATE = "compensation/detail/eco_account/view.html"
|
||||
|
||||
def _get_object(self, id: str):
|
||||
""" Fetch object for detail view
|
||||
|
||||
Args:
|
||||
id (str): The record's id'
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
acc = get_object_or_404(
|
||||
EcoAccount.objects.prefetch_related(
|
||||
"deadlines",
|
||||
).select_related(
|
||||
'geometry',
|
||||
'responsible',
|
||||
),
|
||||
id=id,
|
||||
deleted=None,
|
||||
)
|
||||
return acc
|
||||
|
||||
def _get_detail_context(self, obj: EcoAccount):
|
||||
""" Generate object specific detail context for view
|
||||
|
||||
Args:
|
||||
obj (): The record
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# Order states according to surface
|
||||
before_states = obj.before_states.order_by("-surface")
|
||||
after_states = obj.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 = obj.get_surface_before_states()
|
||||
sum_after_states = obj.get_surface_after_states()
|
||||
diff_states = abs(sum_before_states - sum_after_states)
|
||||
# Calculate rest of available surface for deductions
|
||||
available_total = obj.deductable_rest
|
||||
available_relative = obj.get_deductable_rest_relative()
|
||||
|
||||
# Prefetch related data to decrease the amount of db connections
|
||||
deductions = obj.deductions.filter(
|
||||
intervention__deleted=None,
|
||||
)
|
||||
actions = obj.actions.all()
|
||||
messages.error(request, FORM_INVALID, extra_tags="danger", )
|
||||
|
||||
context = {
|
||||
"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,
|
||||
"deductions": deductions,
|
||||
"actions": actions,
|
||||
"has_finished_deadlines": obj.get_finished_deadlines().exists(),
|
||||
"form": data_form,
|
||||
"geom_form": geom_form,
|
||||
TAB_TITLE_IDENTIFIER: _("Edit {}").format(acc.identifier),
|
||||
}
|
||||
return 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"),
|
||||
)
|
||||
context = BaseContext(request, context).context
|
||||
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from compensation.forms.eco_account import RemoveEcoAccountModalForm
|
||||
from compensation.models import EcoAccount
|
||||
from konova.decorators import shared_access_required
|
||||
from konova.views.remove import AbstractRemoveView
|
||||
|
||||
|
||||
class RemoveEcoAccountView(AbstractRemoveView):
|
||||
_MODEL = EcoAccount
|
||||
_REDIRECT_URL = "compensation:acc:index"
|
||||
_FORM = RemoveEcoAccountModalForm
|
||||
|
||||
@method_decorator(shared_access_required(EcoAccount, "id"))
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
return super().get(request, *args, **kwargs)
|
||||
@@ -5,41 +5,88 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 19.08.22
|
||||
|
||||
"""
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from compensation.models import EcoAccount
|
||||
from compensation.views.compensation.report import BaseCompensationReportView
|
||||
from konova.sub_settings.django_settings import BASE_URL
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.qrcode import QrCode
|
||||
from konova.views.report import AbstractPublicReportView
|
||||
|
||||
|
||||
class EcoAccountReportView(BaseCompensationReportView):
|
||||
_MODEL = EcoAccount
|
||||
class EcoAccountPublicReportView(AbstractPublicReportView):
|
||||
_TEMPLATE = "compensation/report/eco_account/report.html"
|
||||
|
||||
def _get_report_context(self, obj):
|
||||
report_url = BASE_URL + reverse("compensation:acc:report", args=(obj.id,))
|
||||
qrcode_report = QrCode(report_url, 10)
|
||||
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" Renders the public report view
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The id of the intervention
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
acc = get_object_or_404(EcoAccount, id=id)
|
||||
tab_title = _("Report {}").format(acc.identifier)
|
||||
# If intervention is not recorded (yet or currently) we need to render another template without any data
|
||||
if not acc.is_ready_for_publish():
|
||||
template = "report/unavailable.html"
|
||||
context = {
|
||||
TAB_TITLE_IDENTIFIER: tab_title,
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, template, context)
|
||||
|
||||
# Prepare data for map viewer
|
||||
geom_form = SimpleGeomForm(
|
||||
instance=acc
|
||||
)
|
||||
parcels = acc.get_underlying_parcels()
|
||||
|
||||
qrcode = QrCode(
|
||||
content=request.build_absolute_uri(reverse("compensation:acc:report", args=(id,))),
|
||||
size=10
|
||||
)
|
||||
qrcode_lanis = QrCode(
|
||||
content=acc.get_LANIS_link(),
|
||||
size=7
|
||||
)
|
||||
|
||||
# Order states by surface
|
||||
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent")
|
||||
after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent")
|
||||
actions = acc.actions.all().prefetch_related("action_type__parent")
|
||||
|
||||
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier)
|
||||
deductions = obj.deductions.all() \
|
||||
deductions = acc.deductions.all() \
|
||||
.distinct("intervention") \
|
||||
.select_related("intervention") \
|
||||
.values_list("intervention__id", "intervention__identifier", "intervention__title", named=True)
|
||||
|
||||
report_context = {
|
||||
context = {
|
||||
"obj": acc,
|
||||
"qrcode": {
|
||||
"img": qrcode_report.get_img(),
|
||||
"url": qrcode_report.get_content(),
|
||||
"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,
|
||||
}
|
||||
report_context.update(self._get_compensation_report_context(obj))
|
||||
return report_context
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
@@ -5,10 +5,12 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 09.08.21
|
||||
|
||||
"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.urls import reverse
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpRequest
|
||||
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.models import Payment
|
||||
@@ -17,72 +19,97 @@ from konova.decorators import default_group_required, shared_access_required
|
||||
from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED
|
||||
|
||||
|
||||
@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
|
||||
class NewPaymentView(LoginRequiredMixin, View):
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The intervention's id for which a new payment shall be added
|
||||
def __process_request(self, request: HttpRequest, id: str):
|
||||
""" Renders a modal view for adding new payments
|
||||
|
||||
Returns:
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The intervention's id for which a new payment shall be added
|
||||
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
form = NewPaymentForm(request.POST or None, instance=intervention, request=request)
|
||||
return form.process_request(
|
||||
request,
|
||||
msg_success=PAYMENT_ADDED,
|
||||
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
|
||||
)
|
||||
Returns:
|
||||
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
form = NewPaymentForm(request.POST or None, instance=intervention, request=request)
|
||||
return form.process_request(
|
||||
request,
|
||||
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)
|
||||
|
||||
|
||||
@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
|
||||
class RemovePaymentView(LoginRequiredMixin, View):
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The intervention's id
|
||||
payment_id (str): The payment's id
|
||||
def __process_request(self, request: HttpRequest, id: str, payment_id: str):
|
||||
""" Renders a modal view for removing payments
|
||||
|
||||
Returns:
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The intervention's id
|
||||
payment_id (str): The payment's id
|
||||
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
payment = get_object_or_404(Payment, id=payment_id)
|
||||
form = RemovePaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request)
|
||||
return form.process_request(
|
||||
request=request,
|
||||
msg_success=PAYMENT_REMOVED,
|
||||
redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data"
|
||||
)
|
||||
Returns:
|
||||
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
payment = get_object_or_404(Payment, id=payment_id)
|
||||
form = RemovePaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request)
|
||||
return form.process_request(
|
||||
request=request,
|
||||
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)
|
||||
|
||||
|
||||
@login_required
|
||||
@default_group_required
|
||||
@shared_access_required(Intervention, "id")
|
||||
def payment_edit_view(request: HttpRequest, id: str, payment_id: str):
|
||||
""" Renders a modal view for editing payments
|
||||
class EditPaymentView(LoginRequiredMixin, View):
|
||||
def __process_request(self, request: HttpRequest, id: str, payment_id: str):
|
||||
""" Renders a modal view for editing payments
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The intervention's id
|
||||
payment_id (str): The payment's id
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The intervention's id
|
||||
payment_id (str): The payment's id
|
||||
|
||||
Returns:
|
||||
Returns:
|
||||
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
payment = get_object_or_404(Payment, id=payment_id)
|
||||
form = EditPaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request)
|
||||
return form.process_request(
|
||||
request=request,
|
||||
msg_success=PAYMENT_EDITED,
|
||||
redirect_url=reverse("intervention:detail", args=(payment.intervention_id,)) + "#related_data"
|
||||
)
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
payment = get_object_or_404(Payment, id=payment_id)
|
||||
form = EditPaymentModalForm(request.POST or None, instance=intervention, payment=payment, request=request)
|
||||
return form.process_request(
|
||||
request=request,
|
||||
msg_success=PAYMENT_EDITED,
|
||||
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)
|
||||
|
||||
+2
-2
@@ -88,7 +88,7 @@ class EmaTable(BaseTable, TableRenderMixin, TableOrderMixin):
|
||||
txt=value,
|
||||
new_tab=False,
|
||||
)
|
||||
return format_html(html)
|
||||
return format_html(html, None)
|
||||
|
||||
def render_r(self, value, record: Ema):
|
||||
""" Renders the registered column for a EMA
|
||||
@@ -110,4 +110,4 @@ class EmaTable(BaseTable, TableRenderMixin, TableOrderMixin):
|
||||
tooltip=tooltip,
|
||||
icn_filled=recorded,
|
||||
)
|
||||
return format_html(html)
|
||||
return format_html(html, None)
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Resubmission' %}" data-form-url="{% url 'ema:resubmission-create' obj.id %}">
|
||||
{% fa5_icon 'bell' %}
|
||||
</button>
|
||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-form' obj.id %}">
|
||||
{% fa5_icon 'share-alt' %}
|
||||
</button>
|
||||
{% if is_ets_member %}
|
||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Share' %}" data-form-url="{% url 'ema:share-form' obj.id %}">
|
||||
{% fa5_icon 'share-alt' %}
|
||||
</button>
|
||||
{% if obj.recorded %}
|
||||
<button class="btn btn-default btn-modal mr-2" title="{% trans 'Unrecord' %}" data-form-url="{% url 'ema:record' obj.id %}">
|
||||
{% fa5_icon 'bookmark' 'far' %}
|
||||
@@ -28,19 +28,21 @@
|
||||
{% fa5_icon 'bookmark' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
<a href="{% url 'ema:edit' obj.id %}" class="mr-2">
|
||||
<button class="btn btn-default" title="{% trans 'Edit' %}">
|
||||
{% fa5_icon 'edit' %}
|
||||
</button>
|
||||
</a>
|
||||
{% endif %}
|
||||
{% if is_default_member %}
|
||||
<a href="{% url 'ema:edit' obj.id %}" class="mr-2">
|
||||
<button class="btn btn-default" title="{% trans 'Edit' %}">
|
||||
{% fa5_icon 'edit' %}
|
||||
<button class="btn btn-default btn-modal mr-2" data-form-url="{% url 'ema:log' obj.id %}" title="{% trans 'Show log' %}">
|
||||
{% fa5_icon 'history' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% if is_ets_member %}
|
||||
<button class="btn btn-default btn-modal" data-form-url="{% url 'ema:remove' obj.id %}" title="{% trans 'Delete' %}">
|
||||
{% fa5_icon 'trash' %}
|
||||
</button>
|
||||
</a>
|
||||
<button class="btn btn-default btn-modal mr-2" data-form-url="{% url 'ema:log' obj.id %}" title="{% trans 'Show log' %}">
|
||||
{% fa5_icon 'history' %}
|
||||
</button>
|
||||
<button class="btn btn-default btn-modal" data-form-url="{% url 'ema:remove' obj.id %}" title="{% trans 'Delete' %}">
|
||||
{% fa5_icon 'trash' %}
|
||||
</button>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
@@ -118,6 +118,7 @@ class EmaViewTestCase(CompensationViewTestCase):
|
||||
self.index_url,
|
||||
self.detail_url,
|
||||
self.report_url,
|
||||
self.log_url,
|
||||
]
|
||||
fail_urls = [
|
||||
self.new_url,
|
||||
@@ -133,7 +134,6 @@ class EmaViewTestCase(CompensationViewTestCase):
|
||||
self.action_remove_url,
|
||||
self.action_new_url,
|
||||
self.new_doc_url,
|
||||
self.log_url,
|
||||
self.remove_url,
|
||||
]
|
||||
self.assert_url_fail(client, fail_urls)
|
||||
|
||||
+10
-9
@@ -9,27 +9,28 @@ from django.urls import path
|
||||
|
||||
from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView
|
||||
from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView
|
||||
from ema.views.detail import DetailEmaView
|
||||
from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView
|
||||
from ema.views.ema import remove_view, EmaIndexView, \
|
||||
EmaIdentifierGeneratorView, EmaDetailView, EditEmaFormView, NewEmaFormView
|
||||
from ema.views.ema import IndexEmaView, EmaIdentifierGeneratorView, EditEmaView, NewEmaView
|
||||
from ema.views.log import EmaLogView
|
||||
from ema.views.record import EmaRecordView
|
||||
from ema.views.report import EmaReportView
|
||||
from ema.views.remove import RemoveEmaView
|
||||
from ema.views.report import EmaPublicReportView
|
||||
from ema.views.resubmission import EmaResubmissionView
|
||||
from ema.views.share import EmaShareFormView, EmaShareByTokenView
|
||||
from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView
|
||||
|
||||
app_name = "ema"
|
||||
urlpatterns = [
|
||||
path("", EmaIndexView.as_view(), name="index"),
|
||||
path("new/", NewEmaFormView.as_view(), name="new"),
|
||||
path("", IndexEmaView.as_view(), name="index"),
|
||||
path("new/", NewEmaView.as_view(), name="new"),
|
||||
path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"),
|
||||
path("<id>", EmaDetailView.as_view(), name="detail"),
|
||||
path("<id>", DetailEmaView.as_view(), name="detail"),
|
||||
path('<id>/log', EmaLogView.as_view(), name='log'),
|
||||
path('<id>/edit', EditEmaFormView.as_view(), name='edit'),
|
||||
path('<id>/remove', remove_view, name='remove'),
|
||||
path('<id>/edit', EditEmaView.as_view(), name='edit'),
|
||||
path('<id>/remove', RemoveEmaView.as_view(), name='remove'),
|
||||
path('<id>/record', EmaRecordView.as_view(), name='record'),
|
||||
path('<id>/report', EmaReportView.as_view(), name='report'),
|
||||
path('<id>/report', EmaPublicReportView.as_view(), name='report'),
|
||||
path('<id>/resub', EmaResubmissionView.as_view(), name='resubmission-create'),
|
||||
|
||||
path('<id>/state/new', NewEmaStateView.as_view(), name='new-state'),
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse, HttpRequest
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
from ema.models import Ema
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE
|
||||
from konova.views.detail import AbstractDetailView
|
||||
|
||||
|
||||
class DetailEmaView(AbstractDetailView):
|
||||
_TEMPLATE = "ema/detail/view.html"
|
||||
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" Renders the detail view of an EMA
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The EMA id
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
ema = get_object_or_404(Ema, id=id, deleted=None)
|
||||
|
||||
geom_form = SimpleGeomForm(instance=ema)
|
||||
parcels = ema.get_underlying_parcels()
|
||||
_user = request.user
|
||||
is_entry_shared = ema.is_shared_with(_user)
|
||||
|
||||
# Order states according to surface
|
||||
before_states = ema.before_states.all().order_by("-surface")
|
||||
after_states = ema.after_states.all().order_by("-surface")
|
||||
|
||||
# Precalculate logical errors between before- and after-states
|
||||
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
|
||||
sum_before_states = ema.get_surface_before_states()
|
||||
sum_after_states = ema.get_surface_after_states()
|
||||
diff_states = abs(sum_before_states - sum_after_states)
|
||||
|
||||
ema.set_status_messages(request)
|
||||
|
||||
requesting_user_is_only_shared_user = ema.is_only_shared_with(_user)
|
||||
if requesting_user_is_only_shared_user:
|
||||
messages.info(
|
||||
request,
|
||||
DO_NOT_FORGET_TO_SHARE
|
||||
)
|
||||
|
||||
context = {
|
||||
"obj": ema,
|
||||
"geom_form": geom_form,
|
||||
"parcels": parcels,
|
||||
"is_entry_shared": is_entry_shared,
|
||||
"before_states": before_states,
|
||||
"after_states": after_states,
|
||||
"sum_before_states": sum_before_states,
|
||||
"sum_after_states": sum_after_states,
|
||||
"diff_states": diff_states,
|
||||
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
||||
"is_zb_member": _user.in_group(ZB_GROUP),
|
||||
"is_ets_member": _user.in_group(ETS_GROUP),
|
||||
"LANIS_LINK": ema.get_LANIS_link(),
|
||||
TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}",
|
||||
"has_finished_deadlines": ema.get_finished_deadlines().exists(),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
+202
-107
@@ -5,133 +5,228 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 19.08.22
|
||||
|
||||
"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect, render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views.generic.base import View
|
||||
|
||||
from ema.forms import NewEmaForm, EditEmaForm
|
||||
from ema.models import Ema
|
||||
from ema.tables import EmaTable
|
||||
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
|
||||
from konova.forms.modals import RemoveModalForm
|
||||
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \
|
||||
BaseEditSpatialLocatedObjectFormView
|
||||
from konova.views.detail import BaseDetailView
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import shared_access_required, conservation_office_group_required
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
|
||||
GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
|
||||
from konova.views.identifier import AbstractIdentifierGeneratorView
|
||||
from konova.views.index import AbstractIndexView
|
||||
|
||||
|
||||
class EmaIndexView(LoginRequiredMixin, BaseIndexView):
|
||||
_TAB_TITLE = _("EMAs - Overview")
|
||||
_INDEX_TABLE_CLS = EmaTable
|
||||
class IndexEmaView(AbstractIndexView):
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
""" Renders the index view for EMAs
|
||||
|
||||
def _get_queryset(self):
|
||||
qs = Ema.objects.filter(
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
emas = Ema.objects.filter(
|
||||
deleted=None,
|
||||
).order_by(
|
||||
"-modified__timestamp"
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class NewEmaFormView(BaseNewSpatialLocatedObjectFormView):
|
||||
_FORM_CLS = NewEmaForm
|
||||
_MODEL_CLS = Ema
|
||||
_TEMPLATE = "ema/form/view.html"
|
||||
_TAB_TITLE = _("New EMA")
|
||||
_REDIRECT_URL = "ema:detail"
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
# User has to be an ets user
|
||||
return user.is_ets_user()
|
||||
|
||||
|
||||
class EditEmaFormView(BaseEditSpatialLocatedObjectFormView):
|
||||
_MODEL_CLS = Ema
|
||||
_FORM_CLS = EditEmaForm
|
||||
_TEMPLATE = "ema/form/view.html"
|
||||
_REDIRECT_URL = "ema:detail"
|
||||
_TAB_TITLE = _("Edit {}")
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
# User has to be an ets user
|
||||
return user.is_ets_user()
|
||||
|
||||
|
||||
class EmaIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
|
||||
_MODEL_CLS = Ema
|
||||
_REDIRECT_URL = "ema:index"
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
return user.is_ets_user()
|
||||
|
||||
|
||||
class EmaDetailView(BaseDetailView):
|
||||
_MODEL_CLS = Ema
|
||||
_TEMPLATE = "ema/detail/view.html"
|
||||
|
||||
def _get_object(self, id: str):
|
||||
""" Fetch object for detail view
|
||||
|
||||
Args:
|
||||
id (str): The record's id'
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
ema = get_object_or_404(Ema, id=id, deleted=None)
|
||||
return ema
|
||||
|
||||
def _get_detail_context(self, obj: Ema):
|
||||
""" Generate object specific detail context for view
|
||||
|
||||
Args:
|
||||
obj (): The record
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# Order states according to surface
|
||||
before_states = obj.before_states.all().order_by("-surface")
|
||||
after_states = obj.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 = obj.get_surface_before_states()
|
||||
sum_after_states = obj.get_surface_after_states()
|
||||
diff_states = abs(sum_before_states - sum_after_states)
|
||||
|
||||
table = EmaTable(
|
||||
request,
|
||||
queryset=emas
|
||||
)
|
||||
context = {
|
||||
"before_states": before_states,
|
||||
"after_states": after_states,
|
||||
"sum_before_states": sum_before_states,
|
||||
"sum_after_states": sum_after_states,
|
||||
"diff_states": diff_states,
|
||||
"has_finished_deadlines": obj.get_finished_deadlines().exists(),
|
||||
"table": table,
|
||||
TAB_TITLE_IDENTIFIER: _("EMAs - Overview"),
|
||||
}
|
||||
return context
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
class NewEmaView(LoginRequiredMixin, View):
|
||||
_TEMPLATE = "ema/form/view.html"
|
||||
|
||||
@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
|
||||
@method_decorator(conservation_office_group_required)
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
""" GET endpoint
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The EMA's id
|
||||
Renders form for new EMA
|
||||
|
||||
Returns:
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
*args ():
|
||||
**kwargs ():
|
||||
|
||||
"""
|
||||
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"),
|
||||
)
|
||||
Returns:
|
||||
|
||||
"""
|
||||
data_form = NewEmaForm(request.POST or None)
|
||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
||||
context = {
|
||||
"form": data_form,
|
||||
"geom_form": geom_form,
|
||||
TAB_TITLE_IDENTIFIER: _("New EMA"),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
@method_decorator(conservation_office_group_required)
|
||||
def post(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
""" POST endpoint
|
||||
|
||||
Processes submitted form
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
*args ():
|
||||
**kwargs ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
data_form = NewEmaForm(request.POST or None)
|
||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
|
||||
if data_form.is_valid() and geom_form.is_valid():
|
||||
generated_identifier = data_form.cleaned_data.get("identifier", None)
|
||||
ema = data_form.save(request.user, geom_form)
|
||||
if generated_identifier != ema.identifier:
|
||||
messages.info(
|
||||
request,
|
||||
IDENTIFIER_REPLACED.format(
|
||||
generated_identifier,
|
||||
ema.identifier
|
||||
)
|
||||
)
|
||||
messages.success(request, _("EMA {} added").format(ema.identifier))
|
||||
if geom_form.has_geometry_simplified():
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRY_SIMPLIFIED
|
||||
)
|
||||
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||
if num_ignored_geometries > 0:
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||
)
|
||||
return redirect("ema:detail", id=ema.id)
|
||||
else:
|
||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
||||
context = {
|
||||
"form": data_form,
|
||||
"geom_form": geom_form,
|
||||
TAB_TITLE_IDENTIFIER: _("New EMA"),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
class EmaIdentifierGeneratorView(AbstractIdentifierGeneratorView):
|
||||
_MODEL = Ema
|
||||
|
||||
@method_decorator(conservation_office_group_required)
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
return super().get(request, *args, **kwargs)
|
||||
|
||||
class EditEmaView(LoginRequiredMixin, View):
|
||||
_TEMPLATE = "compensation/form/view.html"
|
||||
|
||||
@method_decorator(conservation_office_group_required)
|
||||
@method_decorator(shared_access_required(Ema, "id"))
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" GET endpoint
|
||||
|
||||
Renders form
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The ema identifier
|
||||
*args ():
|
||||
**kwargs ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# 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(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)
|
||||
@method_decorator(shared_access_required(Ema, "id"))
|
||||
def post(self, request: HttpRequest, id:str, *args, **kwargs) -> HttpResponse:
|
||||
""" POST endpoint
|
||||
|
||||
Process submitted forms
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The id of the ema
|
||||
*args ():
|
||||
**kwargs ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# 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 data_form.is_valid() and geom_form.is_valid():
|
||||
# The data form takes the geom form for processing, as well as the performing user
|
||||
ema = data_form.save(request.user, geom_form)
|
||||
messages.success(request, _("EMA {} edited").format(ema.identifier))
|
||||
if geom_form.has_geometry_simplified():
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRY_SIMPLIFIED
|
||||
)
|
||||
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||
if num_ignored_geometries > 0:
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||
)
|
||||
return redirect("ema:detail", id=ema.id)
|
||||
else:
|
||||
messages.error(request, FORM_INVALID, extra_tags="danger", )
|
||||
context = {
|
||||
"form": data_form,
|
||||
"geom_form": geom_form,
|
||||
TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
@@ -18,7 +18,6 @@ class EmaLogView(AbstractLogView):
|
||||
|
||||
@method_decorator(login_required_modal)
|
||||
@method_decorator(login_required)
|
||||
@method_decorator(conservation_office_group_required)
|
||||
@method_decorator(shared_access_required(Ema, "id"))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from ema.models import Ema
|
||||
from konova.decorators import shared_access_required, conservation_office_group_required
|
||||
from konova.views.remove import AbstractRemoveView
|
||||
|
||||
|
||||
class RemoveEmaView(AbstractRemoveView):
|
||||
_MODEL = Ema
|
||||
_REDIRECT_URL = "ema:index"
|
||||
|
||||
@method_decorator(conservation_office_group_required)
|
||||
@method_decorator(shared_access_required(Ema, "id"))
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
return super().get(request, *args, **kwargs)
|
||||
+59
-14
@@ -5,36 +5,81 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 19.08.22
|
||||
|
||||
"""
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from compensation.views.compensation.report import BaseCompensationReportView
|
||||
from ema.models import Ema
|
||||
from konova.sub_settings.django_settings import BASE_URL
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.qrcode import QrCode
|
||||
from konova.views.report import AbstractPublicReportView
|
||||
|
||||
|
||||
class EmaReportView(BaseCompensationReportView):
|
||||
class EmaPublicReportView(AbstractPublicReportView):
|
||||
_TEMPLATE = "ema/report/report.html"
|
||||
_MODEL = Ema
|
||||
|
||||
def _get_report_context(self, obj):
|
||||
report_url = BASE_URL + reverse("ema:report", args=(obj.id,))
|
||||
qrcode_report = QrCode(report_url, 10)
|
||||
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" Renders the public report view
|
||||
|
||||
generic_compensation_report_context = self._get_compensation_report_context(obj)
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The id of the intervention
|
||||
|
||||
report_context = {
|
||||
Returns:
|
||||
|
||||
"""
|
||||
ema = get_object_or_404(Ema, id=id)
|
||||
tab_title = _("Report {}").format(ema.identifier)
|
||||
# If intervention is not recorded (yet or currently) we need to render another template without any data
|
||||
if not ema.is_ready_for_publish():
|
||||
template = "report/unavailable.html"
|
||||
context = {
|
||||
TAB_TITLE_IDENTIFIER: tab_title,
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, template, context)
|
||||
|
||||
# Prepare data for map viewer
|
||||
geom_form = SimpleGeomForm(
|
||||
instance=ema,
|
||||
)
|
||||
parcels = ema.get_underlying_parcels()
|
||||
|
||||
qrcode = QrCode(
|
||||
content=request.build_absolute_uri(reverse("ema:report", args=(id,))),
|
||||
size=10
|
||||
)
|
||||
qrcode_lanis = QrCode(
|
||||
content=ema.get_LANIS_link(),
|
||||
size=7
|
||||
)
|
||||
|
||||
# Order states by surface
|
||||
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||
after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type")
|
||||
actions = ema.actions.all().prefetch_related("action_type")
|
||||
|
||||
context = {
|
||||
"obj": ema,
|
||||
"qrcode": {
|
||||
"img": qrcode_report.get_img(),
|
||||
"url": qrcode_report.get_content(),
|
||||
"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,
|
||||
}
|
||||
report_context.update(generic_compensation_report_context)
|
||||
return report_context
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
@@ -37,6 +37,14 @@ class InterventionAdmin(BaseObjectAdmin):
|
||||
"geometry",
|
||||
]
|
||||
|
||||
def get_actions(self, request):
|
||||
DELETE_ACTION_IDENTIFIER = "delete_selected"
|
||||
actions = super().get_actions(request)
|
||||
|
||||
if DELETE_ACTION_IDENTIFIER in actions:
|
||||
del actions[DELETE_ACTION_IDENTIFIER]
|
||||
|
||||
return actions
|
||||
|
||||
class InterventionDocumentAdmin(AbstractDocumentAdmin):
|
||||
pass
|
||||
|
||||
@@ -172,8 +172,7 @@ class EditEcoAccountDeductionModalForm(NewEcoAccountDeductionModalForm):
|
||||
deduction = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
deduction_id = kwargs.pop("deduction_id", None)
|
||||
self.deduction = EcoAccountDeduction.objects.get(id=deduction_id)
|
||||
self.deduction = kwargs.pop("deduction", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
self.form_title = _("Edit Deduction")
|
||||
form_data = {
|
||||
@@ -253,20 +252,19 @@ class RemoveEcoAccountDeductionModalForm(RemoveModalForm):
|
||||
Can be used for anything, where removing shall be confirmed by the user a second time.
|
||||
|
||||
"""
|
||||
_DEDUCTION_OBJ = None
|
||||
deduction = None
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
deduction_id = kwargs.pop("deduction_id", None)
|
||||
deduction = EcoAccountDeduction.objects.get(id=deduction_id)
|
||||
self._DEDUCTION_OBJ = deduction
|
||||
deduction = kwargs.pop("deduction", None)
|
||||
self.deduction = deduction
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def save(self):
|
||||
with transaction.atomic():
|
||||
self._DEDUCTION_OBJ.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED)
|
||||
self._DEDUCTION_OBJ.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED)
|
||||
self._DEDUCTION_OBJ.delete()
|
||||
self.deduction.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED)
|
||||
self.deduction.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED)
|
||||
self.deduction.delete()
|
||||
|
||||
def check_for_recorded_instance(self):
|
||||
if self._DEDUCTION_OBJ.intervention.is_recorded:
|
||||
if self.deduction.intervention.is_recorded:
|
||||
self.block_form()
|
||||
|
||||
@@ -127,7 +127,7 @@ class InterventionTable(BaseTable, TableRenderMixin, TableOrderMixin):
|
||||
html += self.render_previously_checked_star(
|
||||
tooltip=tooltip,
|
||||
)
|
||||
return format_html(html)
|
||||
return format_html(html, None)
|
||||
|
||||
def render_r(self, value, record: Intervention):
|
||||
""" Renders the recorded column for an intervention
|
||||
@@ -149,5 +149,5 @@ class InterventionTable(BaseTable, TableRenderMixin, TableOrderMixin):
|
||||
tooltip=tooltip,
|
||||
icn_filled=checked,
|
||||
)
|
||||
return format_html(html)
|
||||
return format_html(html, None)
|
||||
|
||||
|
||||
@@ -36,6 +36,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
||||
self.run_check_url = reverse("intervention:check", args=(self.intervention.id,))
|
||||
self.record_url = reverse("intervention:record", args=(self.intervention.id,))
|
||||
self.report_url = reverse("intervention:report", args=(self.intervention.id,))
|
||||
self.compensation_remove_url = reverse("intervention:remove-compensation", args=(self.compensation.intervention.id, self.compensation.id))
|
||||
|
||||
self.deduction.intervention = self.intervention
|
||||
self.deduction.save()
|
||||
@@ -83,6 +84,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
||||
self.revocation_new_url: f"{login_redirect_base}{self.revocation_new_url}",
|
||||
self.revocation_edit_url: f"{login_redirect_base}{self.revocation_edit_url}",
|
||||
self.revocation_remove_url: f"{login_redirect_base}{self.revocation_remove_url}",
|
||||
self.compensation_remove_url: f"{login_redirect_base}{self.compensation_remove_url}",
|
||||
}
|
||||
|
||||
self.assert_url_success(client, success_urls)
|
||||
@@ -124,6 +126,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
||||
self.deduction_new_url,
|
||||
self.deduction_edit_url,
|
||||
self.deduction_remove_url,
|
||||
self.compensation_remove_url,
|
||||
]
|
||||
|
||||
self.assert_url_success(client, success_urls)
|
||||
@@ -162,6 +165,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
||||
self.deduction_new_url,
|
||||
self.deduction_edit_url,
|
||||
self.deduction_remove_url,
|
||||
self.compensation_remove_url,
|
||||
]
|
||||
fail_urls = [
|
||||
self.run_check_url,
|
||||
@@ -212,6 +216,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
||||
self.deduction_new_url,
|
||||
self.deduction_edit_url,
|
||||
self.deduction_remove_url,
|
||||
self.compensation_remove_url,
|
||||
]
|
||||
success_urls_redirect = {
|
||||
self.share_url: self.detail_url
|
||||
@@ -258,6 +263,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
||||
self.deduction_new_url,
|
||||
self.deduction_edit_url,
|
||||
self.deduction_remove_url,
|
||||
self.compensation_remove_url,
|
||||
]
|
||||
success_urls_redirect = {
|
||||
self.share_url: self.detail_url
|
||||
@@ -304,6 +310,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
||||
self.deduction_new_url,
|
||||
self.deduction_edit_url,
|
||||
self.deduction_remove_url,
|
||||
self.compensation_remove_url,
|
||||
]
|
||||
success_urls_redirect = {
|
||||
self.share_url: self.detail_url
|
||||
@@ -350,6 +357,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
||||
self.deduction_new_url,
|
||||
self.deduction_edit_url,
|
||||
self.deduction_remove_url,
|
||||
self.compensation_remove_url,
|
||||
]
|
||||
success_urls_redirect = {
|
||||
self.share_url: self.detail_url
|
||||
@@ -396,6 +404,7 @@ class InterventionViewTestCase(BaseViewTestCase):
|
||||
self.deduction_new_url,
|
||||
self.deduction_edit_url,
|
||||
self.deduction_remove_url,
|
||||
self.compensation_remove_url,
|
||||
]
|
||||
# Define urls where a redirect to a specific location is the proper response
|
||||
success_urls_redirect = {
|
||||
|
||||
+19
-18
@@ -9,40 +9,41 @@ from django.urls import path
|
||||
|
||||
from intervention.autocomplete.intervention import InterventionAutocomplete
|
||||
from intervention.views.check import InterventionCheckView
|
||||
from intervention.views.compensation import remove_compensation_view
|
||||
from intervention.views.compensation import RemoveCompensationFromInterventionView
|
||||
from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \
|
||||
RemoveInterventionDeductionView
|
||||
from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \
|
||||
RemoveInterventionDocumentView, EditInterventionDocumentView
|
||||
from intervention.views.intervention import remove_view, \
|
||||
InterventionIndexView, InterventionIdentifierGeneratorView, InterventionDetailView, NewInterventionFormView, \
|
||||
EditInterventionFormView
|
||||
from intervention.views.intervention import IndexInterventionView, InterventionIdentifierGeneratorView, \
|
||||
NewInterventionView, EditInterventionView
|
||||
from intervention.views.remove import RemoveInterventionView
|
||||
from intervention.views.detail import DetailInterventionView
|
||||
from intervention.views.log import InterventionLogView
|
||||
from intervention.views.record import InterventionRecordView
|
||||
from intervention.views.report import InterventionReportView
|
||||
from intervention.views.report import InterventionPublicReportView
|
||||
from intervention.views.resubmission import InterventionResubmissionView
|
||||
from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \
|
||||
get_revocation_view
|
||||
from intervention.views.revocation import NewInterventionRevocationView, GetInterventionRevocationView, \
|
||||
EditInterventionRevocationView, RemoveInterventionRevocationView
|
||||
from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView
|
||||
|
||||
app_name = "intervention"
|
||||
urlpatterns = [
|
||||
path("", InterventionIndexView.as_view(), name="index"),
|
||||
path('new/', NewInterventionFormView.as_view(), name='new'),
|
||||
path("", IndexInterventionView.as_view(), name="index"),
|
||||
path('new/', NewInterventionView.as_view(), name='new'),
|
||||
path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'),
|
||||
path('<id>', InterventionDetailView.as_view(), name='detail'),
|
||||
path('<id>', DetailInterventionView.as_view(), name='detail'),
|
||||
path('<id>/log', InterventionLogView.as_view(), name='log'),
|
||||
path('<id>/edit', EditInterventionFormView.as_view(), name='edit'),
|
||||
path('<id>/remove', remove_view, name='remove'),
|
||||
path('<id>/edit', EditInterventionView.as_view(), name='edit'),
|
||||
path('<id>/remove', RemoveInterventionView.as_view(), name='remove'),
|
||||
path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'),
|
||||
path('<id>/share', InterventionShareFormView.as_view(), name='share-form'),
|
||||
path('<id>/check', InterventionCheckView.as_view(), name='check'),
|
||||
path('<id>/record', InterventionRecordView.as_view(), name='record'),
|
||||
path('<id>/report', InterventionReportView.as_view(), name='report'),
|
||||
path('<id>/report', InterventionPublicReportView.as_view(), name='report'),
|
||||
path('<id>/resub', InterventionResubmissionView.as_view(), name='resubmission-create'),
|
||||
|
||||
# Compensations
|
||||
path('<id>/compensation/<comp_id>/remove', remove_compensation_view, name='remove-compensation'),
|
||||
path('<id>/compensation/<comp_id>/remove', RemoveCompensationFromInterventionView.as_view(), name='remove-compensation'),
|
||||
|
||||
# Documents
|
||||
path('<id>/document/new/', NewInterventionDocumentView.as_view(), name='new-doc'),
|
||||
@@ -56,10 +57,10 @@ urlpatterns = [
|
||||
path('<id>/deduction/<deduction_id>/remove', RemoveInterventionDeductionView.as_view(), name='remove-deduction'),
|
||||
|
||||
# Revocation routes
|
||||
path('<id>/revocation/new', new_revocation_view, name='new-revocation'),
|
||||
path('<id>/revocation/<revocation_id>/edit', edit_revocation_view, name='edit-revocation'),
|
||||
path('<id>/revocation/<revocation_id>/remove', remove_revocation_view, name='remove-revocation'),
|
||||
path('revocation/<doc_id>', get_revocation_view, name='get-doc-revocation'),
|
||||
path('<id>/revocation/new', NewInterventionRevocationView.as_view(), name='new-revocation'),
|
||||
path('<id>/revocation/<revocation_id>/edit', EditInterventionRevocationView.as_view(), name='edit-revocation'),
|
||||
path('<id>/revocation/<revocation_id>/remove', RemoveInterventionRevocationView.as_view(), name='remove-revocation'),
|
||||
path('revocation/<doc_id>', GetInterventionRevocationView.as_view(), name='get-doc-revocation'),
|
||||
|
||||
# Autocomplete
|
||||
path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="autocomplete"),
|
||||
|
||||
+31
-13
@@ -6,25 +6,43 @@ Created on: 19.08.22
|
||||
|
||||
"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
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.views import View
|
||||
|
||||
from intervention.forms.modals.check import CheckModalForm
|
||||
from intervention.models import Intervention
|
||||
from konova.decorators import registration_office_group_required, shared_access_required
|
||||
from konova.utils.message_templates import INTERVENTION_INVALID
|
||||
from konova.views.base import BaseModalFormView
|
||||
|
||||
class InterventionCheckView(LoginRequiredMixin, View):
|
||||
|
||||
class InterventionCheckView(LoginRequiredMixin, BaseModalFormView):
|
||||
_MODEL_CLS = Intervention
|
||||
_FORM_CLS = CheckModalForm
|
||||
_MSG_SUCCESS = _("Check performed")
|
||||
_MSG_ERROR = INTERVENTION_INVALID
|
||||
_REDIRECT_URL = "intervention:detail"
|
||||
def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" Renders check form for an intervention
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
return user.is_zb_user()
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): Intervention's id
|
||||
|
||||
def _get_redirect_url(self, *args, **kwargs):
|
||||
redirect_url = super()._get_redirect_url(*args, **kwargs)
|
||||
redirect_url += "#related_data"
|
||||
return redirect_url
|
||||
Returns:
|
||||
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
form = CheckModalForm(request.POST or None, instance=intervention, request=request)
|
||||
return form.process_request(
|
||||
request,
|
||||
msg_success=_("Check performed"),
|
||||
msg_error=INTERVENTION_INVALID
|
||||
)
|
||||
|
||||
@method_decorator(registration_office_group_required)
|
||||
@method_decorator(shared_access_required(Intervention, "id"))
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
return self.__process_request(request, id, *args, **kwargs)
|
||||
|
||||
@method_decorator(registration_office_group_required)
|
||||
@method_decorator(shared_access_required(Intervention, "id"))
|
||||
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
return self.__process_request(request, id, *args, **kwargs)
|
||||
|
||||
@@ -5,42 +5,52 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 19.08.22
|
||||
|
||||
"""
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.http import HttpRequest, Http404
|
||||
from django.http import HttpRequest, Http404, HttpResponse
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
|
||||
from intervention.models import Intervention
|
||||
from konova.decorators import shared_access_required, login_required_modal
|
||||
from konova.decorators import shared_access_required, default_group_required
|
||||
from konova.forms.modals import RemoveModalForm
|
||||
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
|
||||
|
||||
|
||||
@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
|
||||
class RemoveCompensationFromInterventionView(LoginRequiredMixin, View):
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The compensation's id
|
||||
def __process_request(self, request: HttpRequest, id: str, comp_id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" Renders a modal view for removing the compensation
|
||||
|
||||
Returns:
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The compensation's id
|
||||
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
try:
|
||||
comp = intervention.compensations.get(
|
||||
id=comp_id
|
||||
Returns:
|
||||
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
try:
|
||||
comp = intervention.compensations.get(
|
||||
id=comp_id
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404("Unknown compensation")
|
||||
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
|
||||
return form.process_request(
|
||||
request=request,
|
||||
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
|
||||
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data",
|
||||
)
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404("Unknown compensation")
|
||||
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
|
||||
return form.process_request(
|
||||
request=request,
|
||||
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
|
||||
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data",
|
||||
)
|
||||
|
||||
@method_decorator(default_group_required)
|
||||
@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(default_group_required)
|
||||
@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)
|
||||
@@ -5,27 +5,51 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 19.08.22
|
||||
|
||||
"""
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from intervention.models import Intervention
|
||||
from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_EDITED, DEDUCTION_REMOVED
|
||||
from konova.decorators import default_group_required, shared_access_required
|
||||
from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView
|
||||
|
||||
_INTERVENTION_DETAIL_URL_NAME = "intervention:detail"
|
||||
|
||||
class NewInterventionDeductionView(LoginRequiredMixin, AbstractNewDeductionView):
|
||||
_MODEL_CLS = Intervention
|
||||
_MSG_SUCCESS = DEDUCTION_ADDED
|
||||
_REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME
|
||||
class NewInterventionDeductionView(AbstractNewDeductionView):
|
||||
def _custom_check(self, obj):
|
||||
pass
|
||||
|
||||
model = Intervention
|
||||
redirect_url = "intervention:detail"
|
||||
|
||||
@method_decorator(login_required)
|
||||
@method_decorator(default_group_required)
|
||||
@method_decorator(shared_access_required(Intervention, "id"))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class EditInterventionDeductionView(LoginRequiredMixin, AbstractEditDeductionView):
|
||||
_MODEL_CLS = Intervention
|
||||
_MSG_SUCCESS = DEDUCTION_EDITED
|
||||
_REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME
|
||||
class EditInterventionDeductionView(AbstractEditDeductionView):
|
||||
def _custom_check(self, obj):
|
||||
pass
|
||||
|
||||
model = Intervention
|
||||
redirect_url = "intervention:detail"
|
||||
|
||||
@method_decorator(login_required)
|
||||
@method_decorator(default_group_required)
|
||||
@method_decorator(shared_access_required(Intervention, "id"))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
|
||||
class RemoveInterventionDeductionView(LoginRequiredMixin, AbstractRemoveDeductionView):
|
||||
_MODEL_CLS = Intervention
|
||||
_MSG_SUCCESS = DEDUCTION_REMOVED
|
||||
_REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME
|
||||
class RemoveInterventionDeductionView(AbstractRemoveDeductionView):
|
||||
def _custom_check(self, obj):
|
||||
pass
|
||||
|
||||
model = Intervention
|
||||
redirect_url = "intervention:detail"
|
||||
|
||||
@method_decorator(login_required)
|
||||
@method_decorator(default_group_required)
|
||||
@method_decorator(shared_access_required(Intervention, "id"))
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from django.contrib import messages
|
||||
from django.http import HttpResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
|
||||
from intervention.models import Intervention
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, DO_NOT_FORGET_TO_SHARE
|
||||
from konova.views.detail import AbstractDetailView
|
||||
|
||||
|
||||
class DetailInterventionView(AbstractDetailView):
|
||||
_TEMPLATE = "intervention/detail/view.html"
|
||||
|
||||
def get(self, request, id: str, *args, **kwargs) -> HttpResponse:
|
||||
# Fetch data, filter out deleted related data
|
||||
intervention = get_object_or_404(
|
||||
Intervention.objects.select_related(
|
||||
"geometry",
|
||||
"legal",
|
||||
"responsible",
|
||||
).prefetch_related(
|
||||
"legal__revocations",
|
||||
),
|
||||
id=id,
|
||||
deleted=None
|
||||
)
|
||||
compensations = intervention.compensations.filter(
|
||||
deleted=None,
|
||||
)
|
||||
_user = request.user
|
||||
is_data_shared = intervention.is_shared_with(user=_user)
|
||||
|
||||
geom_form = SimpleGeomForm(
|
||||
instance=intervention,
|
||||
)
|
||||
last_checked = intervention.get_last_checked_action()
|
||||
last_checked_tooltip = ""
|
||||
if last_checked:
|
||||
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
|
||||
last_checked.get_timestamp_str_formatted(),
|
||||
last_checked.user
|
||||
)
|
||||
|
||||
has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists()
|
||||
|
||||
requesting_user_is_only_shared_user = intervention.is_only_shared_with(_user)
|
||||
if requesting_user_is_only_shared_user:
|
||||
messages.info(
|
||||
request,
|
||||
DO_NOT_FORGET_TO_SHARE
|
||||
)
|
||||
|
||||
context = {
|
||||
"obj": intervention,
|
||||
"last_checked": last_checked,
|
||||
"last_checked_tooltip": last_checked_tooltip,
|
||||
"compensations": compensations,
|
||||
"is_entry_shared": is_data_shared,
|
||||
"geom_form": geom_form,
|
||||
"is_default_member": _user.in_group(DEFAULT_GROUP),
|
||||
"is_zb_member": _user.in_group(ZB_GROUP),
|
||||
"is_ets_member": _user.in_group(ETS_GROUP),
|
||||
"LANIS_LINK": intervention.get_LANIS_link(),
|
||||
"has_payment_without_document": has_payment_without_document,
|
||||
TAB_TITLE_IDENTIFIER: f"{intervention.identifier} - {intervention.title}",
|
||||
}
|
||||
|
||||
request = intervention.set_status_messages(request)
|
||||
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
+177
-150
@@ -8,144 +8,202 @@ Created on: 19.08.22
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, render, redirect
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
|
||||
from intervention.forms.intervention import EditInterventionForm, NewInterventionForm
|
||||
from intervention.models import Intervention
|
||||
from intervention.tables import InterventionTable
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import default_group_required, shared_access_required, login_required_modal
|
||||
from konova.decorators import default_group_required, shared_access_required
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.forms.modals import RemoveModalForm
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \
|
||||
CHECK_STATE_RESET, FORM_INVALID, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
|
||||
from konova.views.base import BaseIndexView, BaseIdentifierGeneratorView, BaseNewSpatialLocatedObjectFormView, \
|
||||
BaseEditSpatialLocatedObjectFormView
|
||||
from konova.views.detail import BaseDetailView
|
||||
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, \
|
||||
CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, GEOMETRY_SIMPLIFIED, \
|
||||
GEOMETRIES_IGNORED_TEMPLATE
|
||||
from konova.views.identifier import AbstractIdentifierGeneratorView
|
||||
from konova.views.index import AbstractIndexView
|
||||
|
||||
|
||||
class InterventionIndexView(LoginRequiredMixin, BaseIndexView):
|
||||
_INDEX_TABLE_CLS = InterventionTable
|
||||
_TAB_TITLE = _("Interventions - Overview")
|
||||
class IndexInterventionView(AbstractIndexView):
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
"""
|
||||
Renders the index view for Interventions
|
||||
|
||||
def _get_queryset(self):
|
||||
qs = Intervention.objects.filter(
|
||||
deleted=None,
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
|
||||
Returns:
|
||||
A rendered view
|
||||
"""
|
||||
# Filtering by user access is performed in table filter inside InterventionTableFilter class
|
||||
interventions = Intervention.objects.filter(
|
||||
deleted=None, # not deleted
|
||||
).select_related(
|
||||
"legal"
|
||||
).order_by(
|
||||
"-modified__timestamp"
|
||||
)
|
||||
return qs
|
||||
|
||||
|
||||
class NewInterventionFormView(BaseNewSpatialLocatedObjectFormView):
|
||||
_MODEL_CLS = Intervention
|
||||
_FORM_CLS = NewInterventionForm
|
||||
_TEMPLATE = "intervention/form/view.html"
|
||||
_REDIRECT_URL = "intervention:detail"
|
||||
_TAB_TITLE = _("New intervention")
|
||||
|
||||
|
||||
class EditInterventionFormView(BaseEditSpatialLocatedObjectFormView):
|
||||
_MODEL_CLS = Intervention
|
||||
_FORM_CLS = EditInterventionForm
|
||||
_TEMPLATE = "intervention/form/view.html"
|
||||
_REDIRECT_URL = "intervention:detail"
|
||||
_TAB_TITLE = _("Edit {}")
|
||||
|
||||
|
||||
class InterventionIdentifierGeneratorView(LoginRequiredMixin, BaseIdentifierGeneratorView):
|
||||
_MODEL_CLS = Intervention
|
||||
_REDIRECT_URL = "intervention:index"
|
||||
|
||||
|
||||
class InterventionDetailView(BaseDetailView):
|
||||
_MODEL_CLS = Intervention
|
||||
_TEMPLATE = "intervention/detail/view.html"
|
||||
|
||||
def _get_object(self, id: str):
|
||||
""" Returns the intervention
|
||||
|
||||
Args:
|
||||
id (str): The intervention's id
|
||||
|
||||
Returns:
|
||||
obj (Intervention): The intervention
|
||||
"""
|
||||
# Fetch data, filter out deleted related data
|
||||
obj = get_object_or_404(
|
||||
self._MODEL_CLS.objects.select_related(
|
||||
"geometry",
|
||||
"legal",
|
||||
"responsible",
|
||||
).prefetch_related(
|
||||
"legal__revocations",
|
||||
),
|
||||
id=id,
|
||||
deleted=None
|
||||
table = InterventionTable(
|
||||
request=request,
|
||||
queryset=interventions
|
||||
)
|
||||
return obj
|
||||
|
||||
def _get_detail_context(self, obj: Intervention):
|
||||
""" Generate object specific detail context for view
|
||||
|
||||
Args:
|
||||
obj (): The record
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
compensations = obj.compensations.filter(deleted=None)
|
||||
last_checked = obj.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 = obj.payments.exists() and not obj.get_documents()[1].exists()
|
||||
context = {
|
||||
"last_checked": last_checked,
|
||||
"last_checked_tooltip": last_checked_tooltip,
|
||||
"compensations": compensations,
|
||||
"has_payment_without_document": has_payment_without_document,
|
||||
"table": table,
|
||||
TAB_TITLE_IDENTIFIER: _("Interventions - Overview"),
|
||||
}
|
||||
return context
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
|
||||
@login_required
|
||||
@default_group_required
|
||||
@shared_access_required(Intervention, "id")
|
||||
def edit_view(request: HttpRequest, id: str):
|
||||
"""
|
||||
Renders a view for editing interventions
|
||||
class NewInterventionView(LoginRequiredMixin, View):
|
||||
_TEMPLATE = "intervention/form/view.html"
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
@method_decorator(default_group_required)
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
|
||||
Returns:
|
||||
"""
|
||||
Renders a view for a new intervention creation
|
||||
|
||||
"""
|
||||
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)
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
|
||||
# Create forms, initialize with values from db/from POST request
|
||||
data_form = EditInterventionForm(request.POST or None, instance=intervention)
|
||||
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
|
||||
if request.method == "POST":
|
||||
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)
|
||||
|
||||
if data_form.is_valid() and geom_form.is_valid():
|
||||
generated_identifier = data_form.cleaned_data.get("identifier", None)
|
||||
intervention = data_form.save(request.user, geom_form)
|
||||
if generated_identifier != intervention.identifier:
|
||||
messages.info(
|
||||
request,
|
||||
IDENTIFIER_REPLACED.format(
|
||||
generated_identifier,
|
||||
intervention.identifier
|
||||
)
|
||||
)
|
||||
|
||||
messages.success(request, _("Intervention {} added").format(intervention.identifier))
|
||||
if geom_form.has_geometry_simplified():
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRY_SIMPLIFIED
|
||||
)
|
||||
|
||||
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||
if num_ignored_geometries > 0:
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||
)
|
||||
return redirect("intervention:detail", id=intervention.id)
|
||||
|
||||
else:
|
||||
messages.error(request, FORM_INVALID, extra_tags="danger", )
|
||||
context = {
|
||||
"form": data_form,
|
||||
"geom_form": geom_form,
|
||||
TAB_TITLE_IDENTIFIER: _("New intervention"),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
|
||||
class InterventionIdentifierGeneratorView(AbstractIdentifierGeneratorView):
|
||||
_MODEL = Intervention
|
||||
|
||||
|
||||
class EditInterventionView(LoginRequiredMixin, View):
|
||||
_TEMPLATE = "intervention/form/view.html"
|
||||
|
||||
@method_decorator(default_group_required)
|
||||
@method_decorator(shared_access_required(Intervention, "id"))
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
"""
|
||||
Renders a view for editing interventions
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The intervention identifier
|
||||
|
||||
Returns:
|
||||
HttpResponse: The rendered view
|
||||
"""
|
||||
|
||||
# Get object from db
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
if intervention.is_recorded:
|
||||
messages.info(
|
||||
request,
|
||||
RECORDED_BLOCKS_EDIT
|
||||
)
|
||||
return redirect("intervention:detail", id=id)
|
||||
|
||||
# Create forms, initialize with values from db/from POST request
|
||||
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():
|
||||
# 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
|
||||
@@ -159,48 +217,17 @@ def edit_view(request: HttpRequest, id: str):
|
||||
request,
|
||||
GEOMETRY_SIMPLIFIED
|
||||
)
|
||||
|
||||
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||
if num_ignored_geometries > 0:
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||
)
|
||||
|
||||
return redirect("intervention:detail", id=intervention.id)
|
||||
else:
|
||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
||||
else:
|
||||
# For clarification: nothing in this case
|
||||
pass
|
||||
context = {
|
||||
"form": data_form,
|
||||
"geom_form": geom_form,
|
||||
TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier),
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, template, context)
|
||||
|
||||
|
||||
@login_required_modal
|
||||
@login_required
|
||||
@default_group_required
|
||||
@shared_access_required(Intervention, "id")
|
||||
def remove_view(request: HttpRequest, id: str):
|
||||
""" Renders a remove view for this intervention
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The uuid id as string
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
obj = Intervention.objects.get(id=id)
|
||||
identifier = obj.identifier
|
||||
form = RemoveModalForm(request.POST or None, instance=obj, request=request)
|
||||
return form.process_request(
|
||||
request,
|
||||
_("{} removed").format(identifier),
|
||||
redirect_url=reverse("intervention:index")
|
||||
)
|
||||
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)
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
|
||||
from intervention.models import Intervention
|
||||
from konova.decorators import shared_access_required
|
||||
from konova.views.remove import AbstractRemoveView
|
||||
|
||||
|
||||
class RemoveInterventionView(AbstractRemoveView):
|
||||
_MODEL = Intervention
|
||||
_REDIRECT_URL = "intervention:index"
|
||||
|
||||
@method_decorator(shared_access_required(Intervention, "id"))
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
return super().get(request, *args, **kwargs)
|
||||
@@ -5,41 +5,78 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 19.08.22
|
||||
|
||||
"""
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.urls import reverse
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from intervention.models import Intervention
|
||||
from konova.sub_settings.django_settings import BASE_URL
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.qrcode import QrCode
|
||||
from konova.views.report import BaseReportView
|
||||
from konova.views.report import AbstractPublicReportView
|
||||
|
||||
|
||||
class InterventionReportView(BaseReportView):
|
||||
_TEMPLATE = 'intervention/report/report.html'
|
||||
_MODEL = Intervention
|
||||
class InterventionPublicReportView(AbstractPublicReportView):
|
||||
_TEMPLATE = "intervention/report/report.html"
|
||||
|
||||
def _get_report_context(self, obj: Intervention):
|
||||
""" Returns the specific context needed for an intervention report
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" Renders the public report view
|
||||
|
||||
Args:
|
||||
obj (Intervention): The object for the report
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The id of the intervention
|
||||
|
||||
Returns:
|
||||
dict: The object specific context for rendering the report
|
||||
"""
|
||||
distinct_deductions = obj.deductions.all().distinct("account")
|
||||
report_url = BASE_URL + reverse("intervention:report", args=(obj.id,))
|
||||
qrcode_report = QrCode(report_url, 10)
|
||||
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
|
||||
|
||||
return {
|
||||
"""
|
||||
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
|
||||
)
|
||||
|
||||
context = {
|
||||
"obj": intervention,
|
||||
"deductions": distinct_deductions,
|
||||
"qrcode": {
|
||||
"img": qrcode_report.get_img(),
|
||||
"url": qrcode_report.get_content(),
|
||||
"img": qrcode.get_img(),
|
||||
"url": qrcode.get_content(),
|
||||
},
|
||||
"qrcode_lanis": {
|
||||
"img": qrcode_lanis.get_img(),
|
||||
"url": qrcode_lanis.get_content(),
|
||||
},
|
||||
"geom_form": geom_form,
|
||||
"parcels": parcels,
|
||||
"tables_scrollable": False,
|
||||
TAB_TITLE_IDENTIFIER: tab_title,
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
@@ -6,10 +6,12 @@ Created on: 19.08.22
|
||||
|
||||
"""
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import HttpRequest
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.shortcuts import get_object_or_404, redirect
|
||||
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, \
|
||||
RemoveRevocationModalForm
|
||||
@@ -19,100 +21,125 @@ from konova.utils.documents import get_document
|
||||
from konova.utils.message_templates import REVOCATION_ADDED, DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED
|
||||
|
||||
|
||||
@login_required
|
||||
@default_group_required
|
||||
@shared_access_required(Intervention, "id")
|
||||
def new_revocation_view(request: HttpRequest, id: str):
|
||||
""" Renders sharing form for an intervention
|
||||
class NewInterventionRevocationView(LoginRequiredMixin, View):
|
||||
def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" Renders sharing form for an intervention
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): Intervention's id
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): Intervention's id
|
||||
|
||||
Returns:
|
||||
Returns:
|
||||
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, request=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(
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
form = NewRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention,
|
||||
request=request)
|
||||
return form.process_request(
|
||||
request,
|
||||
DATA_UNSHARED
|
||||
msg_success=REVOCATION_ADDED,
|
||||
redirect_url=reverse("intervention:detail", args=(id,)) + "#related_data"
|
||||
)
|
||||
return redirect("intervention:detail", id=doc.instance.id)
|
||||
return get_document(doc)
|
||||
|
||||
@method_decorator(default_group_required)
|
||||
@method_decorator(shared_access_required(Intervention, "id"))
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
return self.__process_request(request, id, *args, **kwargs)
|
||||
|
||||
@method_decorator(default_group_required)
|
||||
@method_decorator(shared_access_required(Intervention, "id"))
|
||||
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
return self.__process_request(request, id, *args, **kwargs)
|
||||
|
||||
|
||||
@login_required
|
||||
@default_group_required
|
||||
@shared_access_required(Intervention, "id")
|
||||
def edit_revocation_view(request: HttpRequest, id: str, revocation_id: str):
|
||||
""" Renders a edit view for a revocation
|
||||
class GetInterventionRevocationView(LoginRequiredMixin, View):
|
||||
@method_decorator(default_group_required)
|
||||
def get(self, request: HttpRequest, doc_id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" Returns the revocation document as downloadable file
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The intervention's id as string
|
||||
revocation_id (str): The revocation's id as string
|
||||
Wraps the generic document fetcher function from konova.utils.
|
||||
|
||||
Returns:
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
doc_id (str): The document id
|
||||
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
revocation = get_object_or_404(Revocation, id=revocation_id)
|
||||
Returns:
|
||||
|
||||
form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, revocation=revocation, request=request)
|
||||
return form.process_request(
|
||||
request,
|
||||
REVOCATION_EDITED,
|
||||
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
|
||||
)
|
||||
"""
|
||||
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,
|
||||
DATA_UNSHARED
|
||||
)
|
||||
return redirect("intervention:detail", id=doc.instance.id)
|
||||
return get_document(doc)
|
||||
|
||||
|
||||
@login_required_modal
|
||||
@login_required
|
||||
@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
|
||||
class EditInterventionRevocationView(LoginRequiredMixin, View):
|
||||
def __process_request(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" Renders a edit 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
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The intervention's id as string
|
||||
revocation_id (str): The revocation's id as string
|
||||
|
||||
Returns:
|
||||
Returns:
|
||||
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
revocation = get_object_or_404(Revocation, id=revocation_id)
|
||||
"""
|
||||
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"
|
||||
)
|
||||
form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention,
|
||||
revocation=revocation, request=request)
|
||||
return form.process_request(
|
||||
request,
|
||||
REVOCATION_EDITED,
|
||||
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
|
||||
)
|
||||
|
||||
@method_decorator(default_group_required)
|
||||
@method_decorator(shared_access_required(Intervention, "id"))
|
||||
def get(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
|
||||
return self.__process_request(request, id, revocation_id, *args, **kwargs)
|
||||
|
||||
@method_decorator(default_group_required)
|
||||
@method_decorator(shared_access_required(Intervention, "id"))
|
||||
def post(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
|
||||
return self.__process_request(request, id, revocation_id, *args, **kwargs)
|
||||
|
||||
|
||||
class RemoveInterventionRevocationView(LoginRequiredMixin, View):
|
||||
def __process_request(self, request, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" Renders a remove view for a revocation
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The intervention's id as string
|
||||
revocation_id (str): The revocation's id as string
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
intervention = get_object_or_404(Intervention, id=id)
|
||||
revocation = get_object_or_404(Revocation, id=revocation_id)
|
||||
|
||||
form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation,
|
||||
request=request)
|
||||
return form.process_request(
|
||||
request,
|
||||
REVOCATION_REMOVED,
|
||||
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
|
||||
)
|
||||
|
||||
@method_decorator(default_group_required)
|
||||
@method_decorator(shared_access_required(Intervention, "id"))
|
||||
def get(self, request: HttpRequest, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
|
||||
return self.__process_request(request, id, revocation_id, *args, **kwargs)
|
||||
|
||||
@method_decorator(default_group_required)
|
||||
@method_decorator(shared_access_required(Intervention, "id"))
|
||||
def post(self, request, id: str, revocation_id: str, *args, **kwargs) -> HttpResponse:
|
||||
return self.__process_request(request, id, revocation_id, *args, **kwargs)
|
||||
@@ -25,7 +25,6 @@ class BaseForm(forms.Form):
|
||||
cancel_redirect = None
|
||||
form_caption = None
|
||||
instance = None # The data holding model object
|
||||
user = None # The performing user
|
||||
request = None
|
||||
form_attrs = {} # Holds additional attributes, that can be used in the template
|
||||
has_required_fields = False # Automatically set. Triggers hint rendering in templates
|
||||
@@ -34,7 +33,6 @@ class BaseForm(forms.Form):
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.instance = kwargs.pop("instance", None)
|
||||
self.user = kwargs.pop("user", None)
|
||||
super().__init__(*args, **kwargs)
|
||||
if self.request is not None:
|
||||
self.user = self.request.user
|
||||
@@ -48,7 +46,7 @@ class BaseForm(forms.Form):
|
||||
self.__check_valid_label_input_ratio()
|
||||
|
||||
@abstractmethod
|
||||
def save(self, *arg, **kwargs):
|
||||
def save(self):
|
||||
# To be implemented in subclasses!
|
||||
pass
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ class SimpleGeomForm(BaseForm):
|
||||
disabled=False,
|
||||
)
|
||||
_num_geometries_ignored: int = 0
|
||||
empty = False
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.read_only = kwargs.pop("read_only", True)
|
||||
@@ -49,11 +50,11 @@ class SimpleGeomForm(BaseForm):
|
||||
raise AttributeError
|
||||
|
||||
geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
|
||||
self._set_geojson_properties(geojson, title=self.instance.identifier or None)
|
||||
geojson = self._set_geojson_properties(geojson, title=self.instance.identifier or None)
|
||||
geom = json.dumps(geojson)
|
||||
except AttributeError:
|
||||
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
|
||||
geom = ""
|
||||
geom = json.dumps({})
|
||||
self.empty = True
|
||||
|
||||
self.initialize_form_field("output", geom)
|
||||
@@ -62,17 +63,17 @@ class SimpleGeomForm(BaseForm):
|
||||
super().is_valid()
|
||||
is_valid = True
|
||||
|
||||
# Get geojson from form
|
||||
geom = self.data.get("output", None)
|
||||
if geom is None or len(geom) == 0:
|
||||
# empty geometry is a valid geometry
|
||||
self.cleaned_data["output"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt
|
||||
return is_valid
|
||||
geom = json.loads(geom)
|
||||
# Make sure invalid geometry is properly rendered again to the user
|
||||
# Therefore: write submitted data back into form field
|
||||
# (does not matter whether we know if it is valid or invalid)
|
||||
submitted_data = self.data["output"]
|
||||
submitted_data = json.loads(submitted_data)
|
||||
submitted_data = self._set_geojson_properties(submitted_data)
|
||||
self.initialize_form_field("output", json.dumps(submitted_data))
|
||||
|
||||
# Write submitted data back into form field to make sure invalid geometry
|
||||
# will be rendered again on failed submit
|
||||
self.initialize_form_field("output", self.data["output"])
|
||||
# Get geojson from form for validity checking
|
||||
geom = self.data.get("output", json.dumps({}))
|
||||
geom = json.loads(geom)
|
||||
|
||||
# Initialize features list with empty MultiPolygon, so that an empty input will result in a
|
||||
# proper empty MultiPolygon object
|
||||
@@ -84,20 +85,23 @@ class SimpleGeomForm(BaseForm):
|
||||
"MultiPolygon",
|
||||
"MultiPolygon25D",
|
||||
]
|
||||
# Check validity for each feature of the geometry
|
||||
for feature in features_json:
|
||||
feature_geom = feature.get("geometry", feature)
|
||||
if feature_geom is None:
|
||||
# Fallback for rare cases where a feature does not contain any geometry
|
||||
continue
|
||||
|
||||
# Try to create a geometry object from the single feature
|
||||
feature_geom = json.dumps(feature_geom)
|
||||
g = gdal.OGRGeometry(feature_geom, srs=DEFAULT_SRID_RLP)
|
||||
|
||||
flatten_geometry = g.coord_dim > 2
|
||||
if flatten_geometry:
|
||||
geometry_has_unwanted_dimensions = g.coord_dim > 2
|
||||
if geometry_has_unwanted_dimensions:
|
||||
g = self.__flatten_geom_to_2D(g)
|
||||
|
||||
if g.geom_type not in accepted_ogr_types:
|
||||
geometry_type_is_accepted = g.geom_type not in accepted_ogr_types
|
||||
if geometry_type_is_accepted:
|
||||
self.add_error("output", _("Only surfaces allowed. Points or lines must be buffered."))
|
||||
is_valid &= False
|
||||
return is_valid
|
||||
@@ -109,27 +113,33 @@ class SimpleGeomForm(BaseForm):
|
||||
self._num_geometries_ignored += 1
|
||||
continue
|
||||
|
||||
# Whatever this geometry object is -> try to create a Polygon from it
|
||||
# The resulting polygon object automatically detects whether a valid polygon has been created or not
|
||||
g = Polygon.from_ewkt(g.ewkt)
|
||||
is_valid &= g.valid
|
||||
if not g.valid:
|
||||
self.add_error("output", g.valid_reason)
|
||||
return is_valid
|
||||
|
||||
# If the resulting polygon is just a single polygon, we add it to the list of properly casted features
|
||||
if isinstance(g, Polygon):
|
||||
features.append(g)
|
||||
elif isinstance(g, MultiPolygon):
|
||||
# The resulting polygon could be of type MultiPolygon (due to multiple surfaces)
|
||||
# If so, we extract all polygons from the MultiPolygon and extend the casted features list
|
||||
features.extend(list(g))
|
||||
|
||||
# Unionize all geometry features into one new MultiPolygon
|
||||
# Unionize all polygon features into one new MultiPolygon
|
||||
if features:
|
||||
form_geom = MultiPolygon(*features, srid=DEFAULT_SRID_RLP).unary_union
|
||||
else:
|
||||
# If no features have been processed, this indicates an empty geometry - so we store an empty geometry
|
||||
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
|
||||
|
||||
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
|
||||
form_geom = Geometry.cast_to_multipolygon(form_geom)
|
||||
|
||||
# Write unioned Multipolygon into cleaned data
|
||||
# Write unionized Multipolygon back into cleaned data
|
||||
if self.cleaned_data is None:
|
||||
self.cleaned_data = {}
|
||||
self.cleaned_data["output"] = form_geom.ewkt
|
||||
@@ -252,6 +262,8 @@ class SimpleGeomForm(BaseForm):
|
||||
"""
|
||||
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
|
||||
|
||||
@@ -10,6 +10,7 @@ import json
|
||||
from django.contrib.gis.db.models import MultiPolygonField
|
||||
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
|
||||
from django.db import models, transaction
|
||||
from django.db.models import Q
|
||||
from django.utils import timezone
|
||||
from django.contrib.gis.geos import MultiPolygon
|
||||
|
||||
@@ -102,24 +103,41 @@ class Geometry(BaseResource):
|
||||
resolved_conflicts = all_conflicted_by_conflicts.exclude(id__in=still_conflicting_conflicts)
|
||||
resolved_conflicts.delete()
|
||||
|
||||
def get_data_objects(self):
|
||||
def get_data_objects(self, limit_to_attrs: list = None):
|
||||
""" Getter for all objects which are related to this geometry
|
||||
|
||||
Using the limit_to_attrs we can limit the amount of returned data directly onto the data object attributes
|
||||
we want to have. Reduces memory consumption and runtime.
|
||||
|
||||
Returns:
|
||||
objs (list): The list of objects
|
||||
"""
|
||||
objs = []
|
||||
sets = [
|
||||
|
||||
# Some related data sets can be processed rather easily
|
||||
regular_sets = [
|
||||
self.intervention_set,
|
||||
self.compensation_set,
|
||||
self.ema_set,
|
||||
self.ecoaccount_set,
|
||||
]
|
||||
for _set in sets:
|
||||
for _set in regular_sets:
|
||||
set_objs = _set.filter(
|
||||
deleted=None
|
||||
)
|
||||
objs += set_objs
|
||||
if limit_to_attrs:
|
||||
objs += set_objs.values_list(*limit_to_attrs, flat=True)
|
||||
else:
|
||||
objs += set_objs
|
||||
|
||||
# ... but we need a special treatment for compensations, since they can be deleted directly OR inherit their
|
||||
# de-facto-deleted status from their deleted parent intervention
|
||||
comp_objs = self.compensation_set.filter(
|
||||
Q(deleted=None) & Q(intervention__deleted=None)
|
||||
)
|
||||
if limit_to_attrs:
|
||||
objs += comp_objs.values_list(*limit_to_attrs, flat=True)
|
||||
else:
|
||||
objs += comp_objs
|
||||
return objs
|
||||
|
||||
def get_data_object(self):
|
||||
@@ -397,7 +415,10 @@ class Geometry(BaseResource):
|
||||
"""
|
||||
output_geom = input_geom
|
||||
if not isinstance(input_geom, MultiPolygon):
|
||||
output_geom = MultiPolygon(input_geom, srid=DEFAULT_SRID_RLP)
|
||||
try:
|
||||
output_geom = MultiPolygon(input_geom, srid=DEFAULT_SRID_RLP)
|
||||
except TypeError as e:
|
||||
raise AssertionError(f"Only (Multi)Polygon allowed! Could not convert {input_geom.geom_type} to MultiPolygon")
|
||||
return output_geom
|
||||
|
||||
@staticmethod
|
||||
|
||||
+10
-6
@@ -677,19 +677,23 @@ class GeoReferencedMixin(models.Model):
|
||||
return request
|
||||
|
||||
instance_objs = []
|
||||
conflicts = self.geometry.conflicts_geometries.all()
|
||||
needed_data_object_attrs = [
|
||||
"identifier"
|
||||
]
|
||||
conflicts = self.geometry.conflicts_geometries.iterator()
|
||||
|
||||
for conflict in conflicts:
|
||||
instance_objs += conflict.affected_geometry.get_data_objects()
|
||||
# Only check the affected geometry of this conflict, since we know the conflicting geometry is self.geometry
|
||||
instance_objs += conflict.affected_geometry.get_data_objects(needed_data_object_attrs)
|
||||
|
||||
conflicts = self.geometry.conflicted_by_geometries.all()
|
||||
conflicts = self.geometry.conflicted_by_geometries.iterator()
|
||||
for conflict in conflicts:
|
||||
instance_objs += conflict.conflicting_geometry.get_data_objects()
|
||||
# Only check the conflicting geometry of this conflict, since we know the affected geometry is self.geometry
|
||||
instance_objs += conflict.conflicting_geometry.get_data_objects(needed_data_object_attrs)
|
||||
|
||||
add_message = len(instance_objs) > 0
|
||||
if add_message:
|
||||
instance_identifiers = [x.identifier for x in instance_objs]
|
||||
instance_identifiers = ", ".join(instance_identifiers)
|
||||
instance_identifiers = ", ".join(instance_objs)
|
||||
message_str = GEOMETRY_CONFLICT_WITH_TEMPLATE.format(instance_identifiers)
|
||||
messages.info(request, message_str)
|
||||
return request
|
||||
|
||||
@@ -291,5 +291,5 @@ Overwrites netgis.css attributes
|
||||
}
|
||||
|
||||
.netgis-menu{
|
||||
z-index: 100 !important;
|
||||
z-index: 1 !important;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 47 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 51 KiB |
@@ -11,4 +11,4 @@ BASE_TITLE = "KSP - Kompensationsverzeichnis Service Portal"
|
||||
BASE_FRONTEND_TITLE = "Kompensationsverzeichnis Service Portal"
|
||||
TAB_TITLE_IDENTIFIER = "tab_title"
|
||||
HELP_LINK = "https://dienste.naturschutz.rlp.de/doku/doku.php?id=ksp2:start"
|
||||
IMPRESSUM_LINK = "https://naturschutz.rlp.de/index.php?q=impressum"
|
||||
IMPRESSUM_LINK = "https://naturschutz.rlp.de/ueber-uns/impressum"
|
||||
|
||||
@@ -5,6 +5,9 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
|
||||
Created on: 11.12.23
|
||||
|
||||
"""
|
||||
import json
|
||||
from json import JSONDecodeError
|
||||
|
||||
from django.views.debug import ExceptionReporter
|
||||
|
||||
|
||||
@@ -30,7 +33,7 @@ class KonovaExceptionReporter(ExceptionReporter):
|
||||
"""
|
||||
whitelist = [
|
||||
"is_email",
|
||||
"unicdoe_hint",
|
||||
"unicode_hint",
|
||||
"frames",
|
||||
"request",
|
||||
"user_str",
|
||||
@@ -39,6 +42,8 @@ class KonovaExceptionReporter(ExceptionReporter):
|
||||
"raising_view_name",
|
||||
"exception_type",
|
||||
"exception_value",
|
||||
"filtered_GET_items",
|
||||
"filtered_POST_items",
|
||||
]
|
||||
clean_data = dict()
|
||||
for entry in whitelist:
|
||||
@@ -56,7 +61,28 @@ class KonovaExceptionReporter(ExceptionReporter):
|
||||
"""
|
||||
tb_data = super().get_traceback_data()
|
||||
|
||||
return_data = tb_data
|
||||
if self.is_email:
|
||||
tb_data = self._filter_traceback_data(tb_data)
|
||||
filtered_data = dict()
|
||||
filtered_data.update(self._filter_traceback_data(tb_data))
|
||||
filtered_data.update(self._filter_POST_body(tb_data))
|
||||
return_data = filtered_data
|
||||
return return_data
|
||||
|
||||
return tb_data
|
||||
def _filter_POST_body(self, tb_data: dict):
|
||||
""" Filters POST body from traceback data
|
||||
|
||||
"""
|
||||
post_data = tb_data.get("request", None)
|
||||
if post_data:
|
||||
post_data = post_data.body
|
||||
try:
|
||||
post_data = json.loads(post_data)
|
||||
except JSONDecodeError:
|
||||
pass
|
||||
post_data = {
|
||||
"filtered_POST_items": [
|
||||
("body", post_data),
|
||||
]
|
||||
}
|
||||
return post_data
|
||||
@@ -5,11 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 17.09.21
|
||||
|
||||
"""
|
||||
from uuid import UUID
|
||||
|
||||
from django.contrib import messages
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.http import HttpRequest, Http404
|
||||
|
||||
|
||||
def format_german_float(num) -> str:
|
||||
@@ -24,27 +19,3 @@ def format_german_float(num) -> str:
|
||||
num (str): The number as german Gleitkommazahl
|
||||
"""
|
||||
return format(num, "0,.2f").replace(",", "X").replace(".", ",").replace("X", ".")
|
||||
|
||||
|
||||
def check_user_is_in_any_group(request: HttpRequest):
|
||||
"""
|
||||
Checks for any group membership. Adds a message in case of having none.
|
||||
|
||||
"""
|
||||
user = request.user
|
||||
# Inform user about missing group privileges!
|
||||
groups = user.groups.all()
|
||||
if not groups:
|
||||
messages.info(
|
||||
request,
|
||||
_("+++ Attention: You are not part of any group. You won't be able to create, edit or do anything. Please contact an administrator. +++")
|
||||
)
|
||||
return request
|
||||
|
||||
def check_id_is_valid_uuid(uuid: str):
|
||||
if uuid:
|
||||
try:
|
||||
# Check whether the id is a proper uuid or something that would break a db fetch
|
||||
UUID(uuid)
|
||||
except ValueError:
|
||||
raise Http404
|
||||
|
||||
@@ -5,18 +5,18 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 09.11.20
|
||||
|
||||
"""
|
||||
import random
|
||||
import secrets
|
||||
import string
|
||||
|
||||
|
||||
def generate_token() -> str:
|
||||
def generate_token(length: int = 64) -> str:
|
||||
""" Shortcut for default generating of e.g. API token
|
||||
|
||||
Returns:
|
||||
token (str)
|
||||
"""
|
||||
return generate_random_string(
|
||||
length=64,
|
||||
length=length,
|
||||
use_numbers=True,
|
||||
use_letters_lc=True
|
||||
)
|
||||
@@ -35,6 +35,27 @@ def generate_random_string(length: int, use_numbers: bool = False, use_letters_l
|
||||
elements.append(string.ascii_uppercase)
|
||||
|
||||
elements = "".join(elements)
|
||||
ret_val = "".join(random.choice(elements) for i in range(length))
|
||||
ret_val = "".join(secrets.choice(elements) for i in range(length))
|
||||
return ret_val
|
||||
|
||||
class IdentifierGenerator:
|
||||
_MODEL = None
|
||||
|
||||
def __init__(self, model):
|
||||
from konova.models import BaseObject
|
||||
if not issubclass(model, BaseObject):
|
||||
raise AssertionError("Model must be a subclass of BaseObject!")
|
||||
|
||||
self._MODEL = model
|
||||
|
||||
def generate_id(self) -> str:
|
||||
""" Generates a unique identifier
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
unpersisted_object = self._MODEL()
|
||||
identifier = unpersisted_object.generate_new_identifier()
|
||||
while self._MODEL.objects.filter(identifier=identifier).exists():
|
||||
identifier = unpersisted_object.generate_new_identifier()
|
||||
return identifier
|
||||
|
||||
+21
-8
@@ -5,7 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
|
||||
Created on: 09.11.20
|
||||
|
||||
"""
|
||||
from django.core.mail import send_mail
|
||||
from django.core.mail import send_mail, EmailMultiAlternatives
|
||||
from django.template.loader import render_to_string
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
@@ -45,6 +45,19 @@ class Mailer:
|
||||
auth_password=self.auth_password
|
||||
)
|
||||
|
||||
def send_via_bcc(self, recipient_list: list, subject: str, msg: str):
|
||||
"""
|
||||
Sends a mail with subject and message where recipients will be masked via bcc
|
||||
"""
|
||||
email_obj = EmailMultiAlternatives(
|
||||
subject,
|
||||
msg,
|
||||
self.from_mail,
|
||||
bcc=recipient_list,
|
||||
)
|
||||
email_obj.attach_alternative(msg, "text/html")
|
||||
return email_obj.send(fail_silently=self.fail_silently)
|
||||
|
||||
def send_mail_shared_access_removed(self, obj, user, municipals_names):
|
||||
""" Send a mail if user has no access to the object anymore
|
||||
|
||||
@@ -115,7 +128,7 @@ class Mailer:
|
||||
}
|
||||
msg = render_to_string("email/sharing/shared_access_given_team.html", context)
|
||||
user_mail_address = users_to_notify.values_list("email", flat=True)
|
||||
self.send(
|
||||
self.send_via_bcc(
|
||||
user_mail_address,
|
||||
_("{} - Shared access given").format(obj.identifier),
|
||||
msg
|
||||
@@ -141,7 +154,7 @@ class Mailer:
|
||||
}
|
||||
msg = render_to_string("email/sharing/shared_access_removed_team.html", context)
|
||||
user_mail_address = users_to_notify.values_list("email", flat=True)
|
||||
self.send(
|
||||
self.send_via_bcc(
|
||||
user_mail_address,
|
||||
_("{} - Shared access removed").format(obj.identifier),
|
||||
msg
|
||||
@@ -167,7 +180,7 @@ class Mailer:
|
||||
}
|
||||
msg = render_to_string("email/recording/shared_data_unrecorded_team.html", context)
|
||||
user_mail_address = users_to_notify.values_list("email", flat=True)
|
||||
self.send(
|
||||
self.send_via_bcc(
|
||||
user_mail_address,
|
||||
_("{} - Shared data unrecorded").format(obj.identifier),
|
||||
msg
|
||||
@@ -193,7 +206,7 @@ class Mailer:
|
||||
}
|
||||
msg = render_to_string("email/recording/shared_data_recorded_team.html", context)
|
||||
user_mail_address = users_to_notify.values_list("email", flat=True)
|
||||
self.send(
|
||||
self.send_via_bcc(
|
||||
user_mail_address,
|
||||
_("{} - Shared data recorded").format(obj.identifier),
|
||||
msg
|
||||
@@ -219,7 +232,7 @@ class Mailer:
|
||||
}
|
||||
msg = render_to_string("email/checking/shared_data_checked_team.html", context)
|
||||
user_mail_address = users_to_notify.values_list("email", flat=True)
|
||||
self.send(
|
||||
self.send_via_bcc(
|
||||
user_mail_address,
|
||||
_("{} - Shared data checked").format(obj.identifier),
|
||||
msg
|
||||
@@ -244,7 +257,7 @@ class Mailer:
|
||||
}
|
||||
msg = render_to_string("email/other/deduction_changed_team.html", context)
|
||||
user_mail_address = users_to_notify.values_list("email", flat=True)
|
||||
self.send(
|
||||
self.send_via_bcc(
|
||||
user_mail_address,
|
||||
_("{} - Deduction changed").format(obj.identifier),
|
||||
msg
|
||||
@@ -270,7 +283,7 @@ class Mailer:
|
||||
}
|
||||
msg = render_to_string("email/deleting/shared_data_deleted_team.html", context)
|
||||
user_mail_address = users_to_notify.values_list("email", flat=True)
|
||||
self.send(
|
||||
self.send_via_bcc(
|
||||
user_mail_address,
|
||||
_("{} - Shared data deleted").format(obj.identifier),
|
||||
msg
|
||||
|
||||
+10
-8
@@ -1,6 +1,6 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 17.10.25
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from io import BytesIO
|
||||
@@ -30,15 +30,17 @@ class QrCode:
|
||||
Returns:
|
||||
qrcode_svg (str): The qr code as svg
|
||||
"""
|
||||
img_factory = svg.SvgImage
|
||||
qrcode_img = qrcode.make(
|
||||
content,
|
||||
image_factory=img_factory,
|
||||
qr = qrcode.QRCode(
|
||||
image_factory=qrcode.image.svg.SvgPathImage,
|
||||
box_size=size
|
||||
)
|
||||
stream = BytesIO()
|
||||
qrcode_img.save(stream)
|
||||
return stream.getvalue().decode()
|
||||
qr.add_data(content)
|
||||
qr.make(
|
||||
fit=True
|
||||
)
|
||||
|
||||
img = qr.make_image()
|
||||
return img.to_string(encoding="unicode")
|
||||
|
||||
def get_img(self):
|
||||
return self._img
|
||||
|
||||
@@ -178,7 +178,9 @@ class TableRenderMixin:
|
||||
if len(value) > max_length:
|
||||
value = f"{value[:max_length]}..."
|
||||
value = format_html(
|
||||
f'<div title="{value_orig}">{value}</div>'
|
||||
'<div title="{}">{}</div>',
|
||||
value_orig,
|
||||
value
|
||||
)
|
||||
return value
|
||||
|
||||
@@ -222,7 +224,7 @@ class TableRenderMixin:
|
||||
tooltip=_("Full access granted") if is_entry_shared else _("Access not granted"),
|
||||
icn_class="fas fa-edit rlp-r-inv" if is_entry_shared else "far fa-edit",
|
||||
)
|
||||
return format_html(html)
|
||||
return format_html(html, None)
|
||||
|
||||
|
||||
class TableOrderMixin:
|
||||
|
||||
@@ -1,376 +0,0 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 15.10.25
|
||||
|
||||
"""
|
||||
from abc import abstractmethod
|
||||
|
||||
from bootstrap_modal_forms.mixins import is_ajax
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest, JsonResponse, HttpResponseRedirect
|
||||
from django.shortcuts import render, redirect, get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms import BaseForm, SimpleGeomForm
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.utils.general import check_user_is_in_any_group, check_id_is_valid_uuid
|
||||
from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED, IDENTIFIER_REPLACED, \
|
||||
GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, RECORDED_BLOCKS_EDIT, FORM_INVALID
|
||||
|
||||
|
||||
class BaseView(View):
|
||||
_TEMPLATE: str = "CHANGE_ME"
|
||||
_TAB_TITLE: str = "CHANGE_ME"
|
||||
_REDIRECT_URL: str = "CHANGE_ME"
|
||||
_REDIRECT_URL_ERROR: str = "home"
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
request = check_user_is_in_any_group(request)
|
||||
|
||||
if not self._user_has_permission(request.user):
|
||||
messages.info(request, MISSING_GROUP_PERMISSION)
|
||||
return redirect(reverse(self._REDIRECT_URL_ERROR))
|
||||
|
||||
if not self._user_has_shared_access(request.user, **kwargs):
|
||||
messages.info(request, DATA_UNSHARED)
|
||||
return redirect(reverse(self._REDIRECT_URL_ERROR))
|
||||
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
""" Has to be implemented properly by inheriting classes
|
||||
|
||||
Args:
|
||||
user ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return False
|
||||
|
||||
def _user_has_shared_access(self, user, **kwargs):
|
||||
""" Has to be implemented properly by inheriting classes
|
||||
|
||||
Args:
|
||||
user ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return False
|
||||
|
||||
def _get_redirect_url(self, *args, **kwargs):
|
||||
return self._REDIRECT_URL
|
||||
|
||||
def _get_redirect_url_error(self, *args, **kwargs):
|
||||
return self._REDIRECT_URL_ERROR
|
||||
|
||||
class BaseModalFormView(BaseView):
|
||||
_TEMPLATE = "modal/modal_form.html"
|
||||
_MODEL_CLS = None
|
||||
_FORM_CLS = None
|
||||
_TAB_TITLE = None
|
||||
_MSG_SUCCESS = None
|
||||
_MSG_ERROR = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def _user_has_shared_access(self, user, **kwargs):
|
||||
obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get("id"))
|
||||
return obj.is_shared_with(user)
|
||||
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs):
|
||||
obj = self._MODEL_CLS.objects.get(id=id)
|
||||
form = self._FORM_CLS(request.POST or None, instance=obj, request=request, **kwargs)
|
||||
context = {
|
||||
"form": form,
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
def post(self, request: HttpRequest, id: str, *args, **kwargs):
|
||||
obj = self._MODEL_CLS.objects.get(id=id)
|
||||
form = self._FORM_CLS(request.POST or None, instance=obj, request=request, **kwargs)
|
||||
redirect_url = self._get_redirect_url(obj=obj)
|
||||
if form.is_valid():
|
||||
if not is_ajax(request.META):
|
||||
# Modal forms send one POST for checking on data validity. This can be used to return possible errors
|
||||
# on the form. A second POST (if no errors occured) is sent afterwards and needs to process the
|
||||
# saving/commiting of the data to the database. is_ajax() performs this check. The first request is
|
||||
# an ajax call, the second is a regular form POST.
|
||||
form.save()
|
||||
messages.success(
|
||||
request,
|
||||
self._MSG_SUCCESS
|
||||
)
|
||||
return HttpResponseRedirect(redirect_url)
|
||||
else:
|
||||
context = {
|
||||
"form": form,
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
def _get_redirect_url(self, *args, **kwargs):
|
||||
obj = kwargs.get("obj", None)
|
||||
assert obj is not None
|
||||
return reverse(self._REDIRECT_URL, args=(obj.id,))
|
||||
|
||||
|
||||
class BaseIndexView(BaseView):
|
||||
""" Base class for index views
|
||||
|
||||
"""
|
||||
_TEMPLATE = "generic_index.html"
|
||||
_INDEX_TABLE_CLS = None
|
||||
_REDIRECT_URL = "home"
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
qs = self._get_queryset()
|
||||
table = self._INDEX_TABLE_CLS(
|
||||
request=request,
|
||||
queryset=qs
|
||||
)
|
||||
context = {
|
||||
"table": table,
|
||||
TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
@abstractmethod
|
||||
def _get_queryset(self):
|
||||
raise NotImplementedError
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
# No specific permissions needed for opening base index view
|
||||
return True
|
||||
|
||||
def _user_has_shared_access(self, user, **kwargs):
|
||||
# No specific constraints for shared access of index views
|
||||
return True
|
||||
|
||||
|
||||
class BaseIdentifierGeneratorView(BaseView):
|
||||
_MODEL_CLS = None
|
||||
_REDIRECT_URL: str = "home"
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
tmp_obj = self._MODEL_CLS()
|
||||
identifier = tmp_obj.generate_new_identifier()
|
||||
while self._MODEL_CLS.objects.filter(identifier=identifier).exists():
|
||||
identifier = tmp_obj.generate_new_identifier()
|
||||
return JsonResponse(
|
||||
data={
|
||||
"gen_data": identifier
|
||||
}
|
||||
)
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
""" Should be overwritten in inheriting classes!
|
||||
|
||||
Args:
|
||||
user ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return user.is_default_user()
|
||||
|
||||
def _user_has_shared_access(self, user, **kwargs):
|
||||
# No specific constraints for shared access
|
||||
return True
|
||||
|
||||
|
||||
class BaseFormView(BaseView):
|
||||
_MODEL_CLS = None
|
||||
_FORM_CLS = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def _get_additional_context(self, **kwargs):
|
||||
"""
|
||||
|
||||
Args:
|
||||
**kwargs ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return {}
|
||||
|
||||
|
||||
class BaseSpatialLocatedObjectFormView(LoginRequiredMixin, BaseFormView):
|
||||
_GEOMETRY_FORM_CLS = SimpleGeomForm
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class BaseNewSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
# User has to have default privilege to call this endpoint
|
||||
return user.is_default_user()
|
||||
|
||||
def _user_has_shared_access(self, user, **kwargs):
|
||||
# There is no shared access control since nothing exists yet
|
||||
return True
|
||||
|
||||
def get(self, request: HttpRequest, **kwargs):
|
||||
form: BaseForm = self._FORM_CLS(None, **kwargs, user=request.user)
|
||||
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, user=request.user, read_only=False)
|
||||
|
||||
context = self._get_additional_context()
|
||||
context = BaseContext(request, additional_context=context).context
|
||||
context.update(
|
||||
{
|
||||
"form": form,
|
||||
"geom_form": geom_form,
|
||||
TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
|
||||
}
|
||||
)
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
def post(self, request: HttpRequest, **kwargs):
|
||||
form: BaseForm = self._FORM_CLS(request.POST or None, **kwargs, user=request.user)
|
||||
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(request.POST or None, user=request.user, read_only=False)
|
||||
|
||||
if form.is_valid() and geom_form.is_valid():
|
||||
obj = form.save(request.user, geom_form)
|
||||
obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,))
|
||||
|
||||
generated_identifier = form.cleaned_data.get("identifier", None)
|
||||
|
||||
if generated_identifier != obj.identifier:
|
||||
messages.info(
|
||||
request,
|
||||
IDENTIFIER_REPLACED.format(
|
||||
generated_identifier,
|
||||
obj.identifier
|
||||
)
|
||||
)
|
||||
messages.success(request, _("{} added").format(obj.identifier))
|
||||
if geom_form.has_geometry_simplified():
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRY_SIMPLIFIED
|
||||
)
|
||||
|
||||
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||
if num_ignored_geometries > 0:
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||
)
|
||||
|
||||
return redirect(obj_redirect_url)
|
||||
else:
|
||||
context = self._get_additional_context()
|
||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
||||
|
||||
context = BaseContext(request, additional_context=context).context
|
||||
context.update(
|
||||
{
|
||||
"form": form,
|
||||
"geom_form": geom_form,
|
||||
TAB_TITLE_IDENTIFIER: self._TAB_TITLE,
|
||||
}
|
||||
)
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
|
||||
class BaseEditSpatialLocatedObjectFormView(BaseSpatialLocatedObjectFormView):
|
||||
_TAB_TITLE = _("Edit {}")
|
||||
|
||||
def get(self, request: HttpRequest, id: str):
|
||||
obj = get_object_or_404(
|
||||
self._MODEL_CLS,
|
||||
id=id
|
||||
)
|
||||
obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,))
|
||||
|
||||
if obj.is_recorded:
|
||||
messages.info(
|
||||
request,
|
||||
RECORDED_BLOCKS_EDIT
|
||||
)
|
||||
return redirect(obj_redirect_url)
|
||||
|
||||
form: BaseForm = self._FORM_CLS(None, instance=obj, user=request.user)
|
||||
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, instance=obj, read_only=False)
|
||||
|
||||
context = self._get_additional_context()
|
||||
context = BaseContext(request, additional_context=context).context
|
||||
context.update(
|
||||
{
|
||||
"form": form,
|
||||
"geom_form": geom_form,
|
||||
TAB_TITLE_IDENTIFIER: self._TAB_TITLE.format(obj.identifier),
|
||||
}
|
||||
)
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
def post(self, request: HttpRequest, id: str):
|
||||
obj = get_object_or_404(
|
||||
self._MODEL_CLS,
|
||||
id=id
|
||||
)
|
||||
obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,))
|
||||
|
||||
form: BaseForm = self._FORM_CLS(request.POST or None, instance=obj, user=request.user)
|
||||
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(request.POST or None, instance=obj, read_only=False)
|
||||
|
||||
if form.is_valid() and geom_form.is_valid():
|
||||
obj = form.save(request.user, geom_form)
|
||||
messages.success(request, _("{} edited").format(obj.identifier))
|
||||
|
||||
if geom_form.has_geometry_simplified():
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRY_SIMPLIFIED
|
||||
)
|
||||
|
||||
num_ignored_geometries = geom_form.get_num_geometries_ignored()
|
||||
if num_ignored_geometries > 0:
|
||||
messages.info(
|
||||
request,
|
||||
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
|
||||
)
|
||||
|
||||
return redirect(obj_redirect_url)
|
||||
else:
|
||||
context = self._get_additional_context()
|
||||
messages.error(request, FORM_INVALID, extra_tags="danger",)
|
||||
|
||||
context = BaseContext(request, additional_context=context).context
|
||||
context.update(
|
||||
{
|
||||
"form": form,
|
||||
"geom_form": geom_form,
|
||||
TAB_TITLE_IDENTIFIER: self._TAB_TITLE.format(obj.identifier),
|
||||
}
|
||||
)
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
def _user_has_shared_access(self, user, **kwargs):
|
||||
obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get('id', None))
|
||||
return obj.is_shared_with(user)
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
return user.is_default_user()
|
||||
+92
-54
@@ -6,88 +6,126 @@ Created on: 22.08.22
|
||||
|
||||
"""
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.http import Http404
|
||||
from django.shortcuts import get_object_or_404
|
||||
from django.urls import reverse
|
||||
from django.views import View
|
||||
|
||||
from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \
|
||||
RemoveEcoAccountDeductionModalForm
|
||||
from konova.utils.general import check_id_is_valid_uuid
|
||||
from konova.views.base import BaseModalFormView
|
||||
from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_EDITED, DEDUCTION_REMOVED, DEDUCTION_UNKNOWN
|
||||
|
||||
|
||||
class AbstractDeductionView(BaseModalFormView):
|
||||
_REDIRECT_URL = None
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
check_id_is_valid_uuid(kwargs.get("id"))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
class AbstractDeductionView(View):
|
||||
model = None
|
||||
redirect_url = None
|
||||
|
||||
def _custom_check(self, obj):
|
||||
"""
|
||||
Can be used by inheriting classes to provide custom checks before further processing
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def _user_has_permission(self, user) -> bool:
|
||||
"""
|
||||
|
||||
Args:
|
||||
user ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
return user.is_default_user()
|
||||
|
||||
def _user_has_shared_access(self, user, **kwargs) -> bool:
|
||||
""" A user has shared access on
|
||||
|
||||
Args:
|
||||
user (User): The performing user
|
||||
kwargs (dict): Parameters
|
||||
|
||||
Returns:
|
||||
bool: True if the user has access to the requested object, False otherwise
|
||||
"""
|
||||
ret_val: bool = False
|
||||
try:
|
||||
obj = self._MODEL_CLS.objects.get(
|
||||
id=kwargs.get("id")
|
||||
)
|
||||
ret_val = obj.is_shared_with(user)
|
||||
except ObjectDoesNotExist:
|
||||
ret_val = False
|
||||
return ret_val
|
||||
|
||||
def _get_redirect_url(self, *args, **kwargs):
|
||||
obj = kwargs.get("obj", None)
|
||||
assert obj is not None
|
||||
return reverse(self._REDIRECT_URL, args=(obj.id,)) + "#related_data"
|
||||
raise NotImplementedError("Must be implemented in subclasses")
|
||||
|
||||
|
||||
class AbstractNewDeductionView(AbstractDeductionView):
|
||||
_FORM_CLS = NewEcoAccountDeductionModalForm
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get(self, request, id: str):
|
||||
""" Renders a modal form view for creating deductions
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The obj's id which shall benefit from this deduction
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
obj = get_object_or_404(self.model, id=id)
|
||||
self._custom_check(obj)
|
||||
form = NewEcoAccountDeductionModalForm(request.POST or None, instance=obj, request=request)
|
||||
return form.process_request(
|
||||
request,
|
||||
msg_success=DEDUCTION_ADDED,
|
||||
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data",
|
||||
)
|
||||
|
||||
def post(self, request, id: str):
|
||||
return self.get(request, id)
|
||||
|
||||
|
||||
class AbstractEditDeductionView(AbstractDeductionView):
|
||||
_FORM_CLS = EditEcoAccountDeductionModalForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
check_id_is_valid_uuid(kwargs.get("deduction_id"))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
def _custom_check(self, obj):
|
||||
pass
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get(self, request, id: str, deduction_id: str):
|
||||
""" Renders a modal view for editing deductions
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The object's id
|
||||
deduction_id (str): The deduction's id
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
obj = get_object_or_404(self.model, id=id)
|
||||
self._custom_check(obj)
|
||||
try:
|
||||
eco_deduction = obj.deductions.get(id=deduction_id)
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404(DEDUCTION_UNKNOWN)
|
||||
|
||||
form = EditEcoAccountDeductionModalForm(request.POST or None, instance=obj, deduction=eco_deduction,
|
||||
request=request)
|
||||
return form.process_request(
|
||||
request=request,
|
||||
msg_success=DEDUCTION_EDITED,
|
||||
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
|
||||
)
|
||||
|
||||
def post(self, request, id: str, deduction_id: str):
|
||||
return self.get(request, id, deduction_id)
|
||||
|
||||
|
||||
class AbstractRemoveDeductionView(AbstractDeductionView):
|
||||
_FORM_CLS = RemoveEcoAccountDeductionModalForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
check_id_is_valid_uuid(kwargs.get("deduction_id"))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
def _custom_check(self, obj):
|
||||
pass
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
def get(self, request, id: str, deduction_id: str):
|
||||
""" Renders a modal view for removing deductions
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The object's id
|
||||
deduction_id (str): The deduction's id
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
obj = get_object_or_404(self.model, id=id)
|
||||
self._custom_check(obj)
|
||||
try:
|
||||
eco_deduction = obj.deductions.get(id=deduction_id)
|
||||
except ObjectDoesNotExist:
|
||||
raise Http404(DEDUCTION_UNKNOWN)
|
||||
form = RemoveEcoAccountDeductionModalForm(request.POST or None, instance=obj, deduction=eco_deduction,
|
||||
request=request)
|
||||
return form.process_request(
|
||||
request=request,
|
||||
msg_success=DEDUCTION_REMOVED,
|
||||
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
|
||||
)
|
||||
|
||||
def post(self, request, id: str, deduction_id: str):
|
||||
return self.get(request, id, deduction_id)
|
||||
|
||||
+12
-94
@@ -1,107 +1,25 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 17.10.25
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from abc import abstractmethod
|
||||
from abc import ABC
|
||||
|
||||
from django.contrib import messages
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import render
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
|
||||
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.general import check_id_is_valid_uuid
|
||||
from konova.utils.message_templates import DO_NOT_FORGET_TO_SHARE
|
||||
from konova.views.base import BaseView
|
||||
from konova.decorators import uuid_required, any_group_check
|
||||
|
||||
|
||||
class BaseDetailView(LoginRequiredMixin, BaseView):
|
||||
_MODEL_CLS = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
class AbstractDetailView(LoginRequiredMixin, View, ABC):
|
||||
_TEMPLATE = None
|
||||
|
||||
@method_decorator(uuid_required)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
check_id_is_valid_uuid(kwargs.get('id'))
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def _user_has_shared_access(self, user, **kwargs):
|
||||
""" Check if user has shared access to this object
|
||||
|
||||
Args:
|
||||
user ():
|
||||
**kwargs ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
# Access to an entry's detail view is not restricted by the state of being-shared or not
|
||||
return True
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
# Detail views have no restrictions
|
||||
return True
|
||||
|
||||
def get(self, request: HttpRequest, id: str):
|
||||
""" Get endpoint for detail view
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The record's id
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
obj = self._get_object(id)
|
||||
geom_form = SimpleGeomForm(instance=obj)
|
||||
user = request.user
|
||||
|
||||
requesting_user_is_only_shared_user = obj.is_only_shared_with(user)
|
||||
if requesting_user_is_only_shared_user:
|
||||
messages.info(request, DO_NOT_FORGET_TO_SHARE)
|
||||
|
||||
obj.set_status_messages(request)
|
||||
|
||||
detail_context = self._get_detail_context(obj)
|
||||
context = BaseContext(request, detail_context).context
|
||||
context.update(
|
||||
{
|
||||
"obj": obj,
|
||||
"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": obj.get_LANIS_link(),
|
||||
"is_entry_shared": obj.is_shared_with(user=user),
|
||||
TAB_TITLE_IDENTIFIER: f"{obj.identifier} - {obj.title}"
|
||||
}
|
||||
)
|
||||
return render(request,self._TEMPLATE, context)
|
||||
|
||||
@abstractmethod
|
||||
def _get_detail_context(self, obj):
|
||||
""" Generate object specific detail context for view
|
||||
|
||||
Args:
|
||||
obj (): The record
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
@abstractmethod
|
||||
def _get_object(self, id: str):
|
||||
""" Fetch object for detail view
|
||||
|
||||
Args:
|
||||
id (str): The record's id'
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
raise NotImplementedError
|
||||
@method_decorator(any_group_check)
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
raise NotImplementedError()
|
||||
|
||||
+8
-11
@@ -9,19 +9,21 @@ from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.db.models import Q
|
||||
from django.http import HttpRequest
|
||||
from django.shortcuts import render
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.views import View
|
||||
|
||||
from compensation.models import EcoAccount, Compensation
|
||||
from intervention.models import Intervention
|
||||
from konova.contexts import BaseContext
|
||||
from konova.decorators import any_group_check
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.views.base import BaseView
|
||||
from news.models import ServerMessage
|
||||
|
||||
|
||||
class HomeView(LoginRequiredMixin, BaseView):
|
||||
_TEMPLATE = "konova/home.html"
|
||||
class HomeView(LoginRequiredMixin, View):
|
||||
|
||||
@method_decorator(any_group_check)
|
||||
def get(self, request: HttpRequest):
|
||||
"""
|
||||
Renders the landing page
|
||||
@@ -32,6 +34,7 @@ class HomeView(LoginRequiredMixin, BaseView):
|
||||
Returns:
|
||||
A redirect
|
||||
"""
|
||||
template = "konova/home.html"
|
||||
user = request.user
|
||||
user_teams = user.shared_teams
|
||||
|
||||
@@ -50,6 +53,7 @@ class HomeView(LoginRequiredMixin, BaseView):
|
||||
# Repeat for other objects
|
||||
comps = Compensation.objects.filter(
|
||||
deleted=None,
|
||||
intervention__deleted=None,
|
||||
)
|
||||
user_comps = comps.filter(
|
||||
Q(intervention__users__in=[user]) | Q(intervention__teams__in=user_teams)
|
||||
@@ -72,12 +76,5 @@ class HomeView(LoginRequiredMixin, BaseView):
|
||||
TAB_TITLE_IDENTIFIER: _("Home"),
|
||||
}
|
||||
context = BaseContext(request, additional_context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
return render(request, template, context)
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
# No specific permission needed for home view
|
||||
return True
|
||||
|
||||
def _user_has_shared_access(self, user, **kwargs):
|
||||
# No specific constraint needed for home view
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from abc import ABC
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest, JsonResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
|
||||
from konova.decorators import default_group_required
|
||||
from konova.utils.generators import IdentifierGenerator
|
||||
|
||||
|
||||
class AbstractIdentifierGeneratorView(LoginRequiredMixin, View, ABC):
|
||||
_MODEL = None
|
||||
|
||||
@method_decorator(default_group_required)
|
||||
def get(self, request: HttpRequest, *args, **kwargs):
|
||||
generator = IdentifierGenerator(model=self._MODEL)
|
||||
identifier = generator.generate_id()
|
||||
return JsonResponse(
|
||||
data={
|
||||
"gen_data": identifier
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,21 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from abc import ABC
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
|
||||
from konova.decorators import any_group_check
|
||||
|
||||
|
||||
class AbstractIndexView(LoginRequiredMixin, View, ABC):
|
||||
_TEMPLATE = "generic_index.html"
|
||||
|
||||
@method_decorator(any_group_check)
|
||||
def get(self, request: HttpRequest, *args, **kwargs) -> HttpResponse:
|
||||
raise NotImplementedError()
|
||||
@@ -10,8 +10,9 @@ from json import JSONDecodeError
|
||||
|
||||
import requests
|
||||
import urllib3.util
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.contrib.auth.decorators import login_required
|
||||
from django.http import JsonResponse, HttpRequest, HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.utils.http import urlencode
|
||||
from django.views import View
|
||||
|
||||
@@ -21,13 +22,17 @@ from konova.sub_settings.lanis_settings import MAP_PROXY_HOST_WHITELIST
|
||||
from konova.sub_settings.proxy_settings import PROXIES, GEOPORTAL_RLP_USER, GEOPORTAL_RLP_PASSWORD
|
||||
|
||||
|
||||
class BaseClientProxyView(LoginRequiredMixin, View):
|
||||
class BaseClientProxyView(View):
|
||||
""" Provides proxy functionality for NETGIS map client.
|
||||
|
||||
"""
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@method_decorator(login_required)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def _check_with_whitelist(self, url):
|
||||
parsed_url = urllib3.util.parse_url(url)
|
||||
parsed_url_host = parsed_url.host
|
||||
@@ -62,6 +67,7 @@ class BaseClientProxyView(LoginRequiredMixin, View):
|
||||
|
||||
|
||||
class ClientProxyParcelSearch(BaseClientProxyView):
|
||||
|
||||
def get(self, request: HttpRequest):
|
||||
url = request.META.get("QUERY_STRING")
|
||||
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from abc import ABC
|
||||
|
||||
from django.contrib.auth.mixins import LoginRequiredMixin
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.urls import reverse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from konova.decorators import default_group_required
|
||||
from konova.forms.modals import RemoveModalForm
|
||||
|
||||
|
||||
class AbstractRemoveView(LoginRequiredMixin, View, ABC):
|
||||
_MODEL = None
|
||||
_REDIRECT_URL = None
|
||||
_FORM = RemoveModalForm
|
||||
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
@method_decorator(default_group_required)
|
||||
def __process_request(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
obj = self._MODEL.objects.get(id=id)
|
||||
identifier = obj.identifier
|
||||
form = self._FORM(request.POST or None, instance=obj, request=request)
|
||||
return form.process_request(
|
||||
request,
|
||||
_("{} removed").format(identifier),
|
||||
redirect_url=reverse(self._REDIRECT_URL)
|
||||
)
|
||||
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" GET endpoint for removing via modal form
|
||||
|
||||
Due to the legacy logic of the form (which processes get and post requests directly), we simply need to pipe
|
||||
the request from GET and POST endpoints directly into the same method.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The uuid id as string
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.__process_request(request, id, *args, **kwargs)
|
||||
|
||||
def post(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
""" POST endpoint for removing via modal form
|
||||
|
||||
Due to the legacy logic of the form (which processes get and post requests directly), we simply need to pipe
|
||||
the request from GET and POST endpoints directly into the same method.
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The uuid id as string
|
||||
|
||||
Returns:
|
||||
"""
|
||||
return self.__process_request(request, id, *args, **kwargs)
|
||||
+10
-92
@@ -1,106 +1,24 @@
|
||||
"""
|
||||
Author: Michel Peltriaux
|
||||
Created on: 17.10.25
|
||||
Created on: 14.12.25
|
||||
|
||||
"""
|
||||
from abc import abstractmethod
|
||||
from uuid import UUID
|
||||
from abc import abstractmethod, ABC
|
||||
|
||||
from django.http import HttpRequest, Http404
|
||||
from django.shortcuts import get_object_or_404, render
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
from django.http import HttpRequest, HttpResponse
|
||||
from django.utils.decorators import method_decorator
|
||||
from django.views import View
|
||||
|
||||
from konova.contexts import BaseContext
|
||||
from konova.forms import SimpleGeomForm
|
||||
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
|
||||
from konova.views.base import BaseView
|
||||
from konova.decorators import uuid_required
|
||||
|
||||
|
||||
class BaseReportView(BaseView):
|
||||
class AbstractPublicReportView(View, ABC):
|
||||
_TEMPLATE = None
|
||||
_TAB_TITLE = _("Report {}")
|
||||
_MODEL = None
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
@method_decorator(uuid_required)
|
||||
def dispatch(self, request, *args, **kwargs):
|
||||
# If the given id is not a uuid we act as the result was not found
|
||||
try:
|
||||
UUID(kwargs.get('id'))
|
||||
except ValueError:
|
||||
raise Http404()
|
||||
return super().dispatch(request, *args, **kwargs)
|
||||
|
||||
def _return_unpublishable_content_response(self, request: HttpRequest, tab_title: str):
|
||||
""" Handles HttpResponse return in case the object is not ready for publish
|
||||
|
||||
Args:
|
||||
request ():
|
||||
tab_title ():
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
template = "report/unavailable.html"
|
||||
context = {
|
||||
TAB_TITLE_IDENTIFIER: tab_title,
|
||||
}
|
||||
context = BaseContext(request, context).context
|
||||
return render(request, template, context)
|
||||
|
||||
def get(self, request: HttpRequest, id: str):
|
||||
""" Renders the public report view
|
||||
|
||||
Args:
|
||||
request (HttpRequest): The incoming request
|
||||
id (str): The id of the intervention
|
||||
|
||||
Returns:
|
||||
|
||||
"""
|
||||
obj = get_object_or_404(self._MODEL, id=id)
|
||||
tab_title = self._TAB_TITLE.format(obj.identifier)
|
||||
|
||||
# If object is not recorded we need to render another template without any data
|
||||
if not obj.is_ready_for_publish():
|
||||
return self._return_unpublishable_content_response(request, tab_title)
|
||||
|
||||
# First get specific report context for different types of objects due to inheritance
|
||||
report_context = self._get_report_context(obj)
|
||||
|
||||
# Then generate and add default report context (the same for all models)
|
||||
geom_form = SimpleGeomForm(instance=obj)
|
||||
parcels = obj.get_underlying_parcels()
|
||||
report_context.update(
|
||||
{
|
||||
TAB_TITLE_IDENTIFIER: tab_title,
|
||||
"parcels": parcels,
|
||||
"geom_form": geom_form,
|
||||
"obj": obj
|
||||
}
|
||||
)
|
||||
|
||||
# Then generate the general context based on the report specific data
|
||||
context = BaseContext(request, report_context).context
|
||||
return render(request, self._TEMPLATE, context)
|
||||
|
||||
@abstractmethod
|
||||
def _get_report_context(self, obj):
|
||||
""" Returns the specific context needed for this report view
|
||||
|
||||
Args:
|
||||
obj (RecordableObjectMixin): The object for the report
|
||||
|
||||
Returns:
|
||||
dict: The object specific context for rendering the report
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def _user_has_permission(self, user):
|
||||
# Reports do not need specific permissions to be callable
|
||||
return True
|
||||
|
||||
def _user_has_shared_access(self, user, **kwargs):
|
||||
# Reports do not need specific share states to be callable
|
||||
return True
|
||||
def get(self, request: HttpRequest, id: str, *args, **kwargs) -> HttpResponse:
|
||||
raise NotImplementedError()
|
||||
|
||||
Binary file not shown.
@@ -45,7 +45,7 @@ msgid ""
|
||||
msgstr ""
|
||||
"Project-Id-Version: PACKAGE VERSION\n"
|
||||
"Report-Msgid-Bugs-To: \n"
|
||||
"POT-Creation-Date: 2025-10-19 13:56+0200\n"
|
||||
"POT-Creation-Date: 2025-12-14 17:23+0100\n"
|
||||
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
|
||||
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
|
||||
"Language-Team: LANGUAGE <LL@li.org>\n"
|
||||
@@ -448,54 +448,46 @@ msgid "Select the intervention for which this compensation compensates"
|
||||
msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist"
|
||||
|
||||
#: compensation/forms/compensation.py:114
|
||||
#: compensation/views/compensation/compensation.py:161
|
||||
#: compensation/views/compensation/compensation.py:121
|
||||
msgid "New compensation"
|
||||
msgstr "Neue Kompensation"
|
||||
|
||||
#: compensation/forms/compensation.py:179
|
||||
msgid ""
|
||||
"This intervention is currently recorded. You cannot add further "
|
||||
"compensations as long as it is recorded."
|
||||
msgstr ""
|
||||
"Dieser Eingriff ist derzeit verzeichnet. "
|
||||
"Sie können keine weiteren Kompensationen hinzufügen, so lange er verzeichnet ist."
|
||||
|
||||
#: compensation/forms/compensation.py:202
|
||||
#: compensation/forms/compensation.py:190
|
||||
msgid "Edit compensation"
|
||||
msgstr "Bearbeite Kompensation"
|
||||
|
||||
#: compensation/forms/eco_account.py:31 compensation/utils/quality.py:97
|
||||
#: compensation/forms/eco_account.py:32 compensation/utils/quality.py:97
|
||||
msgid "Available Surface"
|
||||
msgstr "Verfügbare Fläche"
|
||||
|
||||
#: compensation/forms/eco_account.py:34
|
||||
#: compensation/forms/eco_account.py:35
|
||||
msgid "The amount that can be used for deductions"
|
||||
msgstr "Die für Abbuchungen zur Verfügung stehende Menge"
|
||||
|
||||
#: compensation/forms/eco_account.py:43
|
||||
#: compensation/forms/eco_account.py:44
|
||||
#: compensation/templates/compensation/detail/eco_account/view.html:67
|
||||
#: compensation/utils/quality.py:84
|
||||
msgid "Agreement date"
|
||||
msgstr "Vereinbarungsdatum"
|
||||
|
||||
#: compensation/forms/eco_account.py:45
|
||||
#: compensation/forms/eco_account.py:46
|
||||
msgid "When did the parties agree on this?"
|
||||
msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?"
|
||||
|
||||
#: compensation/forms/eco_account.py:72
|
||||
#: compensation/views/eco_account/eco_account.py:93
|
||||
#: compensation/forms/eco_account.py:73
|
||||
#: compensation/views/eco_account/eco_account.py:105
|
||||
msgid "New Eco-Account"
|
||||
msgstr "Neues Ökokonto"
|
||||
|
||||
#: compensation/forms/eco_account.py:81
|
||||
#: compensation/forms/eco_account.py:82
|
||||
msgid "Eco-Account XY; Location ABC"
|
||||
msgstr "Ökokonto XY; Flur ABC"
|
||||
|
||||
#: compensation/forms/eco_account.py:147
|
||||
#: compensation/forms/eco_account.py:148
|
||||
msgid "Edit Eco-Account"
|
||||
msgstr "Ökokonto bearbeiten"
|
||||
|
||||
#: compensation/forms/eco_account.py:183
|
||||
#: compensation/forms/eco_account.py:184
|
||||
msgid ""
|
||||
"{}m² have been deducted from this eco account so far. The given value of {} "
|
||||
"would be too low."
|
||||
@@ -503,12 +495,16 @@ msgstr ""
|
||||
"{}n² wurden bereits von diesem Ökokonto abgebucht. Der eingegebene Wert von "
|
||||
"{} wäre daher zu klein."
|
||||
|
||||
#: compensation/forms/eco_account.py:247
|
||||
#: compensation/forms/eco_account.py:248
|
||||
msgid "The account can not be removed, since there are still deductions."
|
||||
msgstr ""
|
||||
"Das Ökokonto kann nicht entfernt werden, da hierzu noch Abbuchungen "
|
||||
"vorliegen."
|
||||
|
||||
#: compensation/forms/eco_account.py:257
|
||||
msgid "Please contact the responsible conservation office to find a solution!"
|
||||
msgstr "Kontaktieren Sie die zuständige Naturschutzbehörde um eine Lösung zu finden!"
|
||||
|
||||
#: compensation/forms/mixins.py:37
|
||||
#: compensation/templates/compensation/detail/eco_account/view.html:63
|
||||
#: compensation/templates/compensation/report/eco_account/report.html:20
|
||||
@@ -1296,45 +1292,40 @@ msgstr ""
|
||||
msgid "Responsible data"
|
||||
msgstr "Daten zu den verantwortlichen Stellen"
|
||||
|
||||
#: compensation/views/compensation/compensation.py:35
|
||||
#: compensation/views/compensation/compensation.py:52
|
||||
msgid "Compensations - Overview"
|
||||
msgstr "Kompensationen - Übersicht"
|
||||
|
||||
#: compensation/views/compensation/compensation.py:52
|
||||
#, fuzzy
|
||||
#| msgid "New compensation"
|
||||
msgid "New Compensation"
|
||||
msgstr "Neue Kompensation"
|
||||
|
||||
#: compensation/views/compensation/compensation.py:208
|
||||
#: compensation/views/compensation/compensation.py:167
|
||||
#: konova/utils/message_templates.py:40
|
||||
msgid "Compensation {} edited"
|
||||
msgstr "Kompensation {} bearbeitet"
|
||||
|
||||
#: compensation/views/compensation/compensation.py:231
|
||||
#: compensation/views/eco_account/eco_account.py:159 ema/views/ema.py:59
|
||||
#: intervention/views/intervention.py:59 intervention/views/intervention.py:179
|
||||
#: konova/views/base.py:239
|
||||
#: compensation/views/compensation/compensation.py:190
|
||||
#: compensation/views/eco_account/eco_account.py:168 ema/views/ema.py:173
|
||||
#: intervention/views/intervention.py:175
|
||||
msgid "Edit {}"
|
||||
msgstr "Bearbeite {}"
|
||||
|
||||
#: compensation/views/eco_account/eco_account.py:32
|
||||
#: compensation/views/compensation/report.py:35
|
||||
#: compensation/views/eco_account/report.py:35 ema/views/report.py:35
|
||||
#: intervention/views/report.py:36
|
||||
msgid "Report {}"
|
||||
msgstr "Bericht {}"
|
||||
|
||||
#: compensation/views/eco_account/eco_account.py:49
|
||||
msgid "Eco-account - Overview"
|
||||
msgstr "Ökokonten - Übersicht"
|
||||
|
||||
#: compensation/views/eco_account/eco_account.py:70
|
||||
#: compensation/views/eco_account/eco_account.py:82
|
||||
msgid "Eco-Account {} added"
|
||||
msgstr "Ökokonto {} hinzugefügt"
|
||||
|
||||
#: compensation/views/eco_account/eco_account.py:136
|
||||
#: compensation/views/eco_account/eco_account.py:145
|
||||
msgid "Eco-Account {} edited"
|
||||
msgstr "Ökokonto {} bearbeitet"
|
||||
|
||||
#: compensation/views/eco_account/eco_account.py:260
|
||||
msgid "Eco-account removed"
|
||||
msgstr "Ökokonto entfernt"
|
||||
|
||||
#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:42
|
||||
#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:107
|
||||
msgid "New EMA"
|
||||
msgstr "Neue EMA hinzufügen"
|
||||
|
||||
@@ -1362,13 +1353,17 @@ msgstr ""
|
||||
msgid "Payment funded compensation"
|
||||
msgstr "Ersatzzahlungsmaßnahme"
|
||||
|
||||
#: ema/views/ema.py:26
|
||||
#: ema/views/ema.py:52
|
||||
msgid "EMAs - Overview"
|
||||
msgstr "EMAs - Übersicht"
|
||||
|
||||
#: ema/views/ema.py:138
|
||||
msgid "EMA removed"
|
||||
msgstr "EMA entfernt"
|
||||
#: ema/views/ema.py:85
|
||||
msgid "EMA {} added"
|
||||
msgstr "EMA {} hinzugefügt"
|
||||
|
||||
#: ema/views/ema.py:150
|
||||
msgid "EMA {} edited"
|
||||
msgstr "EMA {} bearbeitet"
|
||||
|
||||
#: intervention/forms/intervention.py:49
|
||||
msgid "Construction XY; Location ABC"
|
||||
@@ -1430,7 +1425,7 @@ msgstr "Datum Bestandskraft bzw. Rechtskraft"
|
||||
|
||||
#: intervention/forms/intervention.py:216
|
||||
#: intervention/tests/unit/test_forms.py:36
|
||||
#: intervention/views/intervention.py:51
|
||||
#: intervention/views/intervention.py:109
|
||||
msgid "New intervention"
|
||||
msgstr "Neuer Eingriff"
|
||||
|
||||
@@ -1666,18 +1661,18 @@ msgstr ""
|
||||
msgid "Check performed"
|
||||
msgstr "Prüfung durchgeführt"
|
||||
|
||||
#: intervention/views/intervention.py:33
|
||||
#: intervention/views/intervention.py:53
|
||||
msgid "Interventions - Overview"
|
||||
msgstr "Eingriffe - Übersicht"
|
||||
|
||||
#: intervention/views/intervention.py:154
|
||||
#: intervention/views/intervention.py:86
|
||||
msgid "Intervention {} added"
|
||||
msgstr "Eingriff {} hinzugefügt"
|
||||
|
||||
#: intervention/views/intervention.py:150
|
||||
msgid "Intervention {} edited"
|
||||
msgstr "Eingriff {} bearbeitet"
|
||||
|
||||
#: intervention/views/intervention.py:204
|
||||
msgid "{} removed"
|
||||
msgstr "{} entfernt"
|
||||
|
||||
#: konova/decorators.py:32
|
||||
msgid "You need to be staff to perform this action!"
|
||||
msgstr "Hierfür müssen Sie Mitarbeiter sein!"
|
||||
@@ -1686,7 +1681,7 @@ msgstr "Hierfür müssen Sie Mitarbeiter sein!"
|
||||
msgid "You need to be administrator to perform this action!"
|
||||
msgstr "Hierfür müssen Sie Administrator sein!"
|
||||
|
||||
#: konova/decorators.py:65 konova/utils/general.py:40
|
||||
#: konova/decorators.py:65
|
||||
msgid ""
|
||||
"+++ Attention: You are not part of any group. You won't be able to create, "
|
||||
"edit or do anything. Please contact an administrator. +++"
|
||||
@@ -1798,7 +1793,7 @@ msgstr "Sucht nach Einträgen, an denen diese Person gearbeitet hat"
|
||||
msgid "Save"
|
||||
msgstr "Speichern"
|
||||
|
||||
#: konova/forms/base_form.py:74
|
||||
#: konova/forms/base_form.py:72
|
||||
msgid "Not editable"
|
||||
msgstr "Nicht editierbar"
|
||||
|
||||
@@ -1807,7 +1802,7 @@ msgstr "Nicht editierbar"
|
||||
msgid "Geometry"
|
||||
msgstr "Geometrie"
|
||||
|
||||
#: konova/forms/geometry_form.py:101
|
||||
#: konova/forms/geometry_form.py:105
|
||||
msgid "Only surfaces allowed. Points or lines must be buffered."
|
||||
msgstr ""
|
||||
"Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden."
|
||||
@@ -2308,15 +2303,7 @@ msgstr ""
|
||||
"Dieses Datum ist unrealistisch. Geben Sie bitte das korrekte Datum ein "
|
||||
"(>1950)."
|
||||
|
||||
#: konova/views/base.py:209
|
||||
msgid "{} added"
|
||||
msgstr "{} hinzugefügt"
|
||||
|
||||
#: konova/views/base.py:281
|
||||
msgid "{} edited"
|
||||
msgstr "{} bearbeitet"
|
||||
|
||||
#: konova/views/home.py:72 templates/navbars/navbar.html:16
|
||||
#: konova/views/home.py:75 templates/navbars/navbar.html:16
|
||||
msgid "Home"
|
||||
msgstr "Home"
|
||||
|
||||
@@ -2336,9 +2323,9 @@ msgstr "{} verzeichnet"
|
||||
msgid "Errors found:"
|
||||
msgstr "Fehler gefunden:"
|
||||
|
||||
#: konova/views/report.py:21
|
||||
msgid "Report {}"
|
||||
msgstr "Bericht {}"
|
||||
#: konova/views/remove.py:35
|
||||
msgid "{} removed"
|
||||
msgstr "{} entfernt"
|
||||
|
||||
#: konova/views/resubmission.py:39
|
||||
msgid "Resubmission set"
|
||||
@@ -3066,7 +3053,7 @@ msgid "Manage teams"
|
||||
msgstr ""
|
||||
|
||||
#: user/templates/user/index.html:53 user/templates/user/team/index.html:19
|
||||
#: user/views/views.py:134
|
||||
#: user/views/views.py:135
|
||||
msgid "Teams"
|
||||
msgstr ""
|
||||
|
||||
@@ -3126,40 +3113,34 @@ msgstr "Läuft ab am"
|
||||
msgid "User API token"
|
||||
msgstr "API Nutzer Token"
|
||||
|
||||
#: user/views/views.py:31
|
||||
#: user/views/views.py:33
|
||||
msgid "User settings"
|
||||
msgstr "Einstellungen"
|
||||
|
||||
#: user/views/views.py:44
|
||||
msgid "User notifications"
|
||||
msgstr "Benachrichtigungen"
|
||||
|
||||
#: user/views/views.py:64
|
||||
#: user/views/views.py:59
|
||||
msgid "Notifications edited"
|
||||
msgstr "Benachrichtigungen bearbeitet"
|
||||
|
||||
#: user/views/views.py:152
|
||||
#: user/views/views.py:71
|
||||
msgid "User notifications"
|
||||
msgstr "Benachrichtigungen"
|
||||
|
||||
#: user/views/views.py:147
|
||||
msgid "New team added"
|
||||
msgstr "Neues Team hinzugefügt"
|
||||
|
||||
#: user/views/views.py:167
|
||||
#: user/views/views.py:162
|
||||
msgid "Team edited"
|
||||
msgstr "Team bearbeitet"
|
||||
|
||||
#: user/views/views.py:182
|
||||
#: user/views/views.py:177
|
||||
msgid "Team removed"
|
||||
msgstr "Team gelöscht"
|
||||
|
||||
#: user/views/views.py:197
|
||||
#: user/views/views.py:192
|
||||
msgid "You are not a member of this team"
|
||||
msgstr "Sie sind kein Mitglied dieses Teams"
|
||||
|
||||
#: user/views/views.py:204
|
||||
#: user/views/views.py:199
|
||||
msgid "Left Team"
|
||||
msgstr "Team verlassen"
|
||||
|
||||
#~ msgid "EMA {} added"
|
||||
#~ msgstr "EMA {} hinzugefügt"
|
||||
|
||||
#~ msgid "EMA {} edited"
|
||||
#~ msgstr "EMA {} bearbeitet"
|
||||
|
||||
+52
-50
@@ -1,63 +1,65 @@
|
||||
amqp==5.3.1
|
||||
asgiref==3.8.1
|
||||
asgiref==3.11.1
|
||||
async-timeout==5.0.1
|
||||
beautifulsoup4==4.13.0b2
|
||||
billiard==4.2.1
|
||||
beautifulsoup4==4.14.3
|
||||
billiard==4.2.4
|
||||
cached-property==2.0.1
|
||||
celery==5.4.0
|
||||
certifi==2024.12.14
|
||||
cffi==1.17.1
|
||||
chardet==5.2.0
|
||||
charset-normalizer==3.4.0
|
||||
click==8.1.8
|
||||
celery==5.6.3
|
||||
certifi==2026.4.22
|
||||
cffi==2.0.0
|
||||
chardet==7.4.3
|
||||
charset-normalizer==3.4.7
|
||||
click==8.3.3
|
||||
click-didyoumean==0.3.1
|
||||
click-plugins==1.1.1
|
||||
click-plugins==1.1.1.2
|
||||
click-repl==0.3.0
|
||||
coverage==7.6.9
|
||||
cryptography==44.0.0
|
||||
Deprecated==1.2.15
|
||||
Django==5.1.4
|
||||
django-autocomplete-light==3.11.0
|
||||
coverage==7.13.5
|
||||
cryptography==48.0.0
|
||||
Deprecated==1.3.1
|
||||
Django==6.0.5
|
||||
django-autocomplete-light==4.0.0
|
||||
django-bootstrap-modal-forms==3.0.5
|
||||
django-bootstrap4==24.4
|
||||
django-environ==0.11.2
|
||||
django-filter==24.3
|
||||
django-bootstrap4==26.1
|
||||
django-environ==0.13.0
|
||||
django-filter==25.2
|
||||
django-fontawesome-5==1.0.18
|
||||
django-oauth-toolkit==3.0.1
|
||||
django-tables2==2.7.1
|
||||
django-oauth-toolkit==3.2.0
|
||||
django-tables2==3.0.0
|
||||
et_xmlfile==2.0.0
|
||||
gunicorn==23.0.0
|
||||
idna==3.10
|
||||
importlib_metadata==8.5.0
|
||||
jwcrypto==1.5.6
|
||||
kombu==5.4.0rc1
|
||||
oauthlib==3.2.2
|
||||
gunicorn==26.0.0
|
||||
idna==3.13
|
||||
importlib_metadata==9.0.0
|
||||
itsdangerous==2.2.0
|
||||
jwcrypto==1.5.7
|
||||
kombu==5.6.2
|
||||
oauthlib==3.3.1
|
||||
openpyxl==3.2.0b1
|
||||
packaging==24.2
|
||||
pika==1.3.2
|
||||
pillow==11.0.0
|
||||
prompt_toolkit==3.0.48
|
||||
psycopg==3.2.3
|
||||
psycopg-binary==3.2.3
|
||||
pycparser==2.22
|
||||
pyparsing==3.2.0
|
||||
packaging==26.2
|
||||
pika==1.4.0
|
||||
pillow==12.2.0
|
||||
prompt_toolkit==3.0.52
|
||||
psycopg==3.3.4
|
||||
psycopg-binary==3.3.4
|
||||
pycparser==3.0
|
||||
pyparsing==3.3.2
|
||||
pypng==0.20220715.0
|
||||
pyproj==3.7.0
|
||||
pyproj==3.7.2
|
||||
python-dateutil==2.9.0.post0
|
||||
pytz==2024.2
|
||||
PyYAML==6.0.2
|
||||
qrcode==7.3.1
|
||||
redis==5.1.0b6
|
||||
requests==2.32.3
|
||||
six==1.16.0
|
||||
soupsieve==2.5
|
||||
sqlparse==0.5.1
|
||||
typing_extensions==4.12.2
|
||||
tzdata==2024.2
|
||||
urllib3==2.3.0
|
||||
pytz==2026.2
|
||||
PyYAML==6.0.3
|
||||
qrcode==8.2
|
||||
redis==7.4.0
|
||||
requests==2.33.1
|
||||
six==1.17.0
|
||||
soupsieve==2.8.3
|
||||
sqlparse==0.5.5
|
||||
typing_extensions==4.15.0
|
||||
tzdata==2026.2
|
||||
tzlocal==5.3.1
|
||||
urllib3==2.7.0
|
||||
vine==5.1.0
|
||||
wcwidth==0.2.13
|
||||
wcwidth==0.7.0
|
||||
webservices==0.7
|
||||
wrapt==1.16.0
|
||||
xmltodict==0.14.2
|
||||
zipp==3.21.0
|
||||
wrapt==2.1.2
|
||||
xmltodict==1.0.4
|
||||
zipp==3.23.1
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<div class="jumbotron">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<img src="{% static 'images/error_imgs/croc_technician_400.png' %}" style="max-width: 150px">
|
||||
<img src="{% static 'images/error_imgs/croc_technician_400_sm.png' %}" style="max-width: 150px">
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-9 col-lg-9 col-xl-10">
|
||||
<h1 class="display-4">{% fa5_icon 'question-circle' %}400</h1>
|
||||
|
||||
+1
-1
@@ -5,7 +5,7 @@
|
||||
<div class="jumbotron">
|
||||
<div class="row">
|
||||
<div class="col-auto">
|
||||
<img src="{% static 'images/error_imgs/croc_technician_500.png' %}" style="max-width: 150px">
|
||||
<img src="{% static 'images/error_imgs/croc_technician_500_sm.png' %}" style="max-width: 150px">
|
||||
</div>
|
||||
<div class="col-sm-12 col-md-9 col-lg-9 col-xl-10">
|
||||
<h1 class="display-4">{% fa5_icon 'fire-alt' %} 500</h1>
|
||||
|
||||
@@ -70,7 +70,7 @@
|
||||
</div>
|
||||
<div class="dropdown-menu dropdown-menu-right">
|
||||
{% for rpp_option in table.results_per_page_choices %}
|
||||
<a class="dropdown-item {% if table.results_per_page_chosen == rpp_option %}selected{% endif %}" href="{% querystring table.results_per_page_parameter=rpp_option %}">
|
||||
<a class="dropdown-item {% if table.results_per_page_chosen == rpp_option %}selected{% endif %}" href="{% querystring %}&{{ table.results_per_page_parameter}}={{rpp_option }}">
|
||||
{{ rpp_option }}
|
||||
</a>
|
||||
{% endfor %}
|
||||
|
||||
@@ -58,7 +58,7 @@
|
||||
[
|
||||
{ "id": "webatlas_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "WebatlasRP farbig", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_WebAtlasRP/MapServer/WmsServer?", "name": "RP_WebAtlasRP", "active": true},
|
||||
{ "id": "webatlas_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "WebatlasRP grau", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_ETRS_Gt/MapServer/WmsServer?", "name": "0", "active": false },
|
||||
{ "id": "luftbilder", "folder": "bg", "type": "WMS", "order": -1, "title": "Luftbilder", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/dop_basis.fcgi?", "name": "rp_dop", "active": false },
|
||||
{ "id": "luftbilder", "folder": "bg", "type": "WMS", "order": -1, "title": "Luftbilder", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/rp_dop20.fcgi?", "name": "rp_dop20", "active": false },
|
||||
{ "id": "basemap_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "BasemapDE farbig", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_farbe", "active": false },
|
||||
{ "id": "basemap_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "BasemapDE grau", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_grau", "active": false },
|
||||
{ "id": "dtk_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "DTK5 farbig", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/dtk5_rp.fcgi?", "name": "rp_dtk5", "active": false },
|
||||
@@ -188,6 +188,7 @@
|
||||
{
|
||||
"title": "Ebene hinzufügen",
|
||||
"preview": true,
|
||||
"editable": true,
|
||||
"wms_options": [ "https://sgx.geodatenzentrum.de/wms_topplus_open" ],
|
||||
"wfs_options": [ "http://213.139.159.34:80/geoserver/uesg/wfs" ],
|
||||
"wfs_proxy": "/client/proxy?",
|
||||
|
||||
+1
-1
File diff suppressed because one or more lines are too long
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user