Compare commits

..

119 Commits

Author SHA1 Message Date
8126781b77 Merge pull request 'master' (#496) from master into Docker
Reviewed-on: #496
2025-10-23 16:13:04 +02:00
a6a66d7499 Merge pull request 'master' (#493) from master into Docker
Reviewed-on: #493
2025-10-21 15:59:53 +02:00
1c0b67693d Merge pull request 'master' (#489) from master into Docker
Reviewed-on: #489
2025-10-15 09:51:46 +02:00
ce6bb6b23b Merge pull request 'master' (#486) from master into Docker
Reviewed-on: #486
2025-10-12 11:32:27 +02:00
0b8176db2e Merge pull request 'master' (#484) from master into Docker
Reviewed-on: #484
2025-09-22 12:37:34 +02:00
3a299a040a Merge pull request 'master' (#482) from master into Docker
Reviewed-on: #482
2025-08-18 08:47:01 +02:00
3c5206139b Merge pull request 'master' (#477) from master into Docker
Reviewed-on: #477
2025-05-12 15:40:21 +02:00
6c53f39a28 Merge pull request 'master' (#474) from master into Docker
Reviewed-on: #474
2025-03-28 16:41:31 +01:00
64d8f47174 Merge pull request 'Docker_enhanced' (#472) from Docker_enhanced into Docker
Reviewed-on: #472
2025-03-28 16:11:18 +01:00
f5f3246e89 # Docker enhancements
* optimizes nginx.conf
   * better logging of proxied requests
2025-03-24 14:17:08 +01:00
ad8961ab82 # Docker enhancements
* optimizes nginx.conf
   * better proxy pipelining
* optimizes Dockerfile
   * smaller resulting image
   * faster rebuilding due to reusing of existing layers
* optimizes docker-entrypoint.sh
   * better startup performance
   * better compatibility with docker engine
2025-03-24 13:52:31 +01:00
c2c8630c82 Merge pull request 'master' (#471) from master into Docker
Reviewed-on: #471
2025-02-14 15:33:19 +01:00
dce9e1fc71 # Enhancements
* increases nginx max POST body size to 25MB (document upload)
* limits package requests on version 2.32 due to dependency of kombu to this version
2025-01-24 16:21:55 +01:00
2b84bab1d0 Merge pull request 'master' (#469) from master into Docker
Reviewed-on: #469
2025-01-24 16:12:33 +01:00
303583daa1 Merge pull request 'master' (#466) from master into Docker
Reviewed-on: #466
2025-01-21 13:44:14 +01:00
d07b2ffbfb Merge pull request 'master' (#463) from master into Docker
Reviewed-on: #463
2025-01-08 16:05:15 +01:00
335800c44b Merge pull request 'master' (#459) from master into Docker
Reviewed-on: #459
2024-12-23 13:42:36 +01:00
5766cfde47 Merge pull request 'master' (#454) from master into Docker
Reviewed-on: #454
2024-12-23 12:09:47 +01:00
2ed3fcc0f9 Merge pull request 'master' (#449) from master into Docker
Reviewed-on: #449
2024-11-13 16:09:48 +01:00
bf72295615 Merge pull request 'master' (#447) from master into Docker
Reviewed-on: #447
2024-10-26 10:25:06 +02:00
6b860f8ea5 Merge pull request 'master' (#445) from master into Docker
Reviewed-on: #445
2024-10-26 09:48:50 +02:00
2fa2fa547b Merge pull request 'master' (#443) from master into Docker
Reviewed-on: #443
2024-10-25 19:27:23 +02:00
3de956872c Merge pull request 'master' (#441) from master into Docker
Reviewed-on: #441
2024-10-25 14:24:55 +02:00
1c8e3992d6 Merge pull request 'master' (#438) from master into Docker
Reviewed-on: #438
2024-08-26 18:57:35 +02:00
e6e9e141c8 Merge pull request 'master' (#436) from master into Docker
Reviewed-on: #436
2024-08-19 18:35:17 +02:00
f8ece06ee8 Merge pull request 'master' (#431) from master into Docker
Reviewed-on: #431
2024-08-07 12:07:22 +02:00
149a351bfd Merge pull request 'master' (#429) from master into Docker
Reviewed-on: #429
2024-08-07 12:02:21 +02:00
0164717b8e Merge pull request 'master' (#426) from master into Docker
Reviewed-on: #426
2024-08-06 14:28:41 +02:00
104952bfc3 Merge pull request 'master' (#423) from master into Docker
Reviewed-on: #423
2024-07-10 09:30:30 +02:00
f96241c8d1 Merge pull request 'master' (#421) from master into Docker
Reviewed-on: #421
2024-07-10 09:27:09 +02:00
ac6b534f58 Merge pull request 'master' (#418) from master into Docker
Reviewed-on: #418
2024-07-08 18:44:22 +02:00
06910cd69a # Image tag
* increases image tag
2024-07-05 10:56:10 +02:00
a48ba520fc Merge branch 'refs/heads/master' into Docker
# Conflicts:
#	konova/celery.py
2024-07-05 10:52:13 +02:00
9f18aa5890 Merge pull request 'master' (#414) from master into Docker
Reviewed-on: #414
2024-07-04 11:42:20 +02:00
ab3bd84f3b # Docker enhancement
* adds logging for gunicorn by default
* adds image tagging
* drops docker-compose environment setting in favor of .env usage (needs to be copied from .env.sample)
2024-07-04 09:37:13 +02:00
f829cd5a4c Merge branch 'refs/heads/master' into Docker
# Conflicts:
#	konova/sub_settings/django_settings.py
#	konova/sub_settings/sso_settings.py
#	requirements.txt
2024-07-04 09:30:41 +02:00
0f2bf95b71 Merge pull request 'master' (#409) from master into Docker
Reviewed-on: #409
2024-06-18 11:50:43 +02:00
6a307016ec Merge pull request 'master' (#403) from master into Docker
Reviewed-on: #403
2024-05-17 10:59:17 +02:00
51017ef8fa Merge pull request 'master' (#401) from master into Docker
Reviewed-on: #401
2024-05-17 07:54:16 +02:00
05560534bc Merge pull request 'master' (#399) from master into Docker
Reviewed-on: #399
2024-05-16 17:37:56 +02:00
c882173e78 Merge branch 'refs/heads/master' into Docker
# Conflicts:
#	konova/sub_settings/sso_settings.py
#	requirements.txt
2024-05-16 15:22:57 +02:00
1d94211428 # Requirements update
* fixes requirements.txt due to django-simple-sso dependency
2024-04-12 08:51:18 +02:00
37357080d8 # Gunicorn update
* updates gunicorn package
2024-04-12 08:08:27 +02:00
5afa13ac92 Merge branch 'refs/heads/master' into Docker
# Conflicts:
#	requirements.txt
2024-04-12 08:07:05 +02:00
416cad1c8f Merge pull request 'master' (#392) from master into Docker
Reviewed-on: SGD-Nord/konova#392
2024-04-02 08:11:08 +02:00
b5f83b7163 Merge pull request '# Requirements' (#390) from master into Docker
Reviewed-on: SGD-Nord/konova#390
2024-03-11 08:22:35 +01:00
20cfb5f345 Merge pull request 'master' (#389) from master into Docker
Reviewed-on: SGD-Nord/konova#389
2024-02-29 18:39:41 +01:00
88c96b95f2 Merge pull request '# HOTFIX' (#388) from master into Docker
Reviewed-on: SGD-Nord/konova#388
2024-02-21 18:32:16 +01:00
f6c500b02a Merge pull request 'master' (#387) from master into Docker
Reviewed-on: SGD-Nord/konova#387
2024-02-16 10:16:43 +01:00
d702cd8716 Merge pull request 'master' (#385) from master into Docker
Reviewed-on: SGD-Nord/konova#385
2024-02-16 08:45:32 +01:00
329cdd4838 Merge pull request 'Docker_django5' (#380) from Docker_django5 into Docker
Reviewed-on: SGD-Nord/konova#380
2024-01-05 17:39:48 +01:00
1b70024a29 # Python bullseye
* adds -bullseye to base docker package to ensure backwards compatibility
2024-01-05 17:38:58 +01:00
58206853ee Django5
* changes python dependency
2024-01-05 10:05:27 +01:00
6356398c40 Merge pull request 'master' (#379) from master into Docker_django5
Reviewed-on: SGD-Nord/konova#379
2024-01-05 09:47:53 +01:00
8519922d78 Merge pull request 'Netgis map client fix' (#376) from master into Docker
Reviewed-on: SGD-Nord/konova#376
2023-12-28 15:13:11 +01:00
5ac0654fd4 Merge pull request 'master' (#375) from master into Docker
Reviewed-on: SGD-Nord/konova#375
2023-12-13 13:38:00 +01:00
6c07a81b4f Merge pull request 'master' (#372) from master into Docker
Reviewed-on: SGD-Nord/konova#372
2023-12-12 07:35:17 +01:00
ba45b4f961 Merge pull request 'HOTFIX netgis client' (#367) from master into Docker
Reviewed-on: SGD-Nord/konova#367
2023-12-07 06:44:47 +01:00
280de82a52 Merge pull request 'Hotfix map client edit errors' (#366) from master into Docker
Reviewed-on: SGD-Nord/konova#366
2023-12-05 07:29:41 +01:00
6022e2d879 Merge pull request '# Hotfix netgis client' (#365) from master into Docker
Reviewed-on: SGD-Nord/konova#365
2023-12-05 07:06:19 +01:00
1996efcc0d Docker-compose fix
* drops local volume usage in favor of copied code into container
2023-11-30 12:44:50 +01:00
80569119cb Merge branch 'master' into Docker
# Conflicts:
#	requirements.txt
2023-11-30 12:43:44 +01:00
98e71d4e8a Merge pull request 'HOTFIX' (#355) from master into Docker
Reviewed-on: SGD-Nord/konova#355
2023-11-07 16:27:11 +01:00
fec7191ac2 Merge pull request 'HOTFIX' (#354) from master into Docker
Reviewed-on: SGD-Nord/konova#354
2023-10-26 07:27:19 +02:00
9b1085f206 Merge pull request 'HOTFIX' (#353) from master into Docker
Reviewed-on: SGD-Nord/konova#353
2023-10-26 07:23:21 +02:00
b35d175a5c Merge pull request 'master' (#351) from master into Docker
Reviewed-on: SGD-Nord/konova#351
2023-10-25 10:10:14 +02:00
7f5fb022ac Merge pull request 'master' (#348) from master into Docker
Reviewed-on: SGD-Nord/konova#348
2023-09-15 13:21:38 +02:00
2d3314ab18 Merge pull request 'master' (#344) from master into Docker
Reviewed-on: SGD-Nord/konova#344
2023-08-25 15:06:32 +02:00
8b489f013d Merge pull request 'master' (#341) from master into Docker
Reviewed-on: SGD-Nord/konova#341
2023-08-09 07:30:18 +02:00
16ce5506d8 Merge pull request 'master' (#336) from master into Docker
Reviewed-on: SGD-Nord/konova#336
2023-05-17 14:40:17 +02:00
e440bf8372 Merge pull request 'master' (#333) from master into Docker
Reviewed-on: SGD-Nord/konova#333
2023-05-16 14:11:20 +02:00
607db267e6 Merge pull request 'master' (#330) from master into Docker
Reviewed-on: SGD-Nord/konova#330
2023-04-26 11:30:22 +02:00
352ca64e09 Merge pull request 'master' (#327) from master into Docker
Reviewed-on: SGD-Nord/konova#327
2023-04-19 15:25:05 +02:00
f2b735da6e Merge pull request 'master' (#324) from master into Docker
Reviewed-on: SGD-Nord/konova#324
2023-03-30 15:12:47 +02:00
6f7cfb713e Merge pull request 'master' (#321) from master into Docker
Reviewed-on: SGD-Nord/konova#321
2023-03-28 13:53:14 +02:00
103b703ee9 Merge pull request 'master' (#318) from master into Docker
Reviewed-on: SGD-Nord/konova#318
2023-03-24 07:14:34 +01:00
daf8b1dce6 Merge pull request 'master' (#316) from master into Docker
Reviewed-on: SGD-Nord/konova#316
2023-03-22 09:00:33 +01:00
c088affd74 Merge pull request 'master' (#313) from master into Docker
Reviewed-on: SGD-Nord/konova#313
2023-03-16 08:14:37 +01:00
ecc727c991 Merge pull request 'master' (#311) from master into Docker
Reviewed-on: SGD-Nord/konova#311
2023-03-13 07:00:49 +01:00
632569fa5d Merge pull request 'master' (#307) from master into Docker
Reviewed-on: SGD-Nord/konova#307
2023-02-23 15:35:07 +01:00
6c6cbb7396 Merge pull request 'HOTFIX' (#305) from master into Docker
Reviewed-on: SGD-Nord/konova#305
2023-02-23 12:03:23 +01:00
5e6bfdf77e Merge pull request 'HOTFIX' (#304) from master into Docker
Reviewed-on: SGD-Nord/konova#304
2023-02-23 10:45:57 +01:00
35e5e18b79 Merge pull request 'master' (#303) from master into Docker
Reviewed-on: SGD-Nord/konova#303
2023-02-23 10:24:38 +01:00
c0e8c6bd84 Merge pull request 'master' (#298) from master into Docker
Reviewed-on: SGD-Nord/konova#298
2023-02-21 08:07:47 +01:00
64541b76c5 Merge pull request 'master' (#295) from master into Docker
Reviewed-on: SGD-Nord/konova#295
2023-02-13 14:42:17 +01:00
f65b9262cb Merge pull request 'master' (#292) from master into Docker
Reviewed-on: SGD-Nord/konova#292
2023-02-06 15:01:50 +01:00
2765d0548e Merge pull request 'Quality Check Command enhancement' (#288) from master into Docker
Reviewed-on: SGD-Nord/konova#288
2023-02-01 14:17:21 +01:00
951f810ce5 Merge pull request 'master' (#287) from master into Docker
Reviewed-on: SGD-Nord/konova#287
2023-02-01 14:10:04 +01:00
d2c177d448 Merge pull request 'master' (#283) from master into Docker
Reviewed-on: SGD-Nord/konova#283
2022-12-22 07:56:02 +01:00
299727a7b4 Merge pull request 'master' (#279) from master into Docker
Reviewed-on: SGD-Nord/konova#279
2022-12-14 16:37:41 +01:00
b97976b2c5 Merge pull request 'master' (#276) from master into Docker
Reviewed-on: SGD-Nord/konova#276
2022-12-13 06:50:43 +01:00
20241661ff Merge pull request 'master' (#273) from master into Docker
Reviewed-on: SGD-Nord/konova#273
2022-12-09 13:02:46 +01:00
ad5c0bea67 Merge pull request 'master' (#270) from master into Docker
Reviewed-on: SGD-Nord/konova#270
2022-12-09 07:25:30 +01:00
80a44277bc Merge pull request 'Hotfix: Resubmission mail' (#265) from master into Docker
Reviewed-on: SGD-Nord/konova#265
2022-12-05 06:55:53 +01:00
5c2b5affc9 Merge pull request 'master' (#264) from master into Docker
Reviewed-on: SGD-Nord/konova#264
2022-12-05 06:10:26 +01:00
cd99743d1e Merge pull request 'master' (#261) from master into Docker
Reviewed-on: SGD-Nord/konova#261
2022-12-02 06:43:28 +01:00
b39432be1a Merge pull request 'master' (#259) from master into Docker
Reviewed-on: SGD-Nord/konova#259
2022-12-01 14:01:40 +01:00
03f9a33e54 Merge pull request 'Hotfix' (#256) from master into Docker
Reviewed-on: SGD-Nord/konova#256
2022-12-01 07:05:50 +01:00
699a9c1e76 Merge pull request 'Revert "File number public reports"' (#254) from master into Docker
Reviewed-on: SGD-Nord/konova#254
2022-11-28 13:52:34 +01:00
4dfd02291e Merge pull request 'master' (#253) from master into Docker
Reviewed-on: SGD-Nord/konova#253
2022-11-28 07:29:36 +01:00
e7ca485a88 Merge pull request 'master' (#247) from master into Docker
Reviewed-on: SGD-Nord/konova#247
2022-11-23 16:07:21 +01:00
8319cbfe17 Merge pull request 'master' (#245) from master into Docker
Reviewed-on: SGD-Nord/konova#245
2022-11-23 07:14:15 +01:00
4a023e9f10 Merge pull request 'Hotfix' (#242) from master into Docker
Reviewed-on: SGD-Nord/konova#242
2022-11-18 16:22:50 +01:00
4100f96dc6 Merge pull request 'master' (#241) from master into Docker
Reviewed-on: SGD-Nord/konova#241
2022-11-18 16:17:39 +01:00
ca24f098e4 Merge pull request 'master' (#236) from master into Docker
Reviewed-on: SGD-Nord/konova#236
2022-11-18 06:53:27 +01:00
80dcd62199 Merge pull request 'master' (#234) from master into Docker
Reviewed-on: SGD-Nord/konova#234
2022-11-17 06:55:28 +01:00
0cfd3da728 Merge pull request 'master' (#227) from master into Docker
Reviewed-on: SGD-Nord/konova#227
2022-11-14 07:23:43 +01:00
e141851a87 Merge pull request 'master' (#224) from master into Docker
Reviewed-on: SGD-Nord/konova#224
2022-10-19 07:35:31 +02:00
89ec67999b Merge pull request 'master' (#221) from master into Docker
Reviewed-on: SGD-Nord/konova#221
2022-10-12 09:02:49 +02:00
ec38daaedc Merge pull request 'master' (#219) from master into Docker
Reviewed-on: SGD-Nord/konova#219
2022-10-11 16:41:06 +02:00
45c0826a84 Merge pull request 'master' (#215) from master into Docker
Reviewed-on: SGD-Nord/konova#215
2022-10-05 11:03:03 +02:00
45a383cf85 Merge pull request 'master' (#213) from master into Docker
Reviewed-on: SGD-Nord/konova#213
2022-09-29 10:46:30 +02:00
90aff209f9 Merge pull request 'master' (#210) from master into Docker
Reviewed-on: SGD-Nord/konova#210
2022-09-28 12:28:49 +02:00
13528e91e9 Merge pull request 'master' (#207) from master into Docker
Reviewed-on: SGD-Nord/konova#207
2022-09-16 12:13:59 +02:00
04179d633c Merge pull request 'master' (#205) from master into Docker
Reviewed-on: SGD-Nord/konova#205
2022-09-16 07:24:21 +02:00
0a241305d3 Merge pull request 'Docker Update' (#199) from master into Docker
Reviewed-on: SGD-Nord/konova#199
2022-08-15 11:23:09 +02:00
31565a0bc4 Merge pull request 'Docker_tmp' (#188) from Docker_tmp into Docker
Reviewed-on: SGD-Nord/konova#188
2022-08-02 09:54:31 +02:00
af747417d3 Revert "Revert "Merge branch 'Docker' into master""
This reverts commit 1c38acea25.
2022-08-02 09:44:25 +02:00
c6606c4151 Merge pull request 'master' (#187) from master into Docker_tmp
Reviewed-on: SGD-Nord/konova#187
2022-08-02 09:43:04 +02:00
127 changed files with 12009 additions and 3601 deletions

36
Dockerfile Normal file
View File

@@ -0,0 +1,36 @@
# Nutze ein schlankes Python-Image
FROM python:3.11-slim-bullseye
ENV PYTHONUNBUFFERED 1
WORKDIR /konova
# Installiere System-Abhängigkeiten
RUN apt-get update && apt-get install -y --no-install-recommends \
gdal-bin redis-server nginx \
&& rm -rf /var/lib/apt/lists/* # Platz sparen
# Erstelle benötigte Verzeichnisse & setze Berechtigungen
RUN mkdir -p /var/log/nginx /var/log/gunicorn /var/lib/nginx /tmp/nginx_client_body \
&& touch /var/log/nginx/access.log /var/log/nginx/error.log \
&& chown -R root:root /var/log/nginx /var/lib/nginx /tmp/nginx_client_body
# Kopiere und installiere Python-Abhängigkeiten
COPY ./requirements.txt /konova/
RUN pip install --upgrade pip && pip install --no-cache-dir -r requirements.txt
# Entferne Standard-Nginx-Site und ersetze sie durch eigene Config
RUN rm -rf /etc/nginx/sites-enabled/default
COPY ./nginx.conf /etc/nginx/conf.d
# Kopiere restliche Projektdateien
COPY . /konova/
# Sammle statische Dateien
RUN python manage.py collectstatic --noinput
# Exponiere Ports
#EXPOSE 80 6379 8000
# Setze Entrypoint
ENTRYPOINT ["/konova/docker-entrypoint.sh"]

View File

@@ -4,6 +4,7 @@ the database postgresql and the css library bootstrap as well as the icon packag
fontawesome for a modern look, following best practices from the industry. fontawesome for a modern look, following best practices from the industry.
## Background processes ## Background processes
### !!! For non-docker run
Konova uses celery for background processing. To start the worker you need to run Konova uses celery for background processing. To start the worker you need to run
```shell ```shell
$ celery -A konova worker -l INFO $ celery -A konova worker -l INFO
@@ -18,3 +19,58 @@ Technical documention is provided in the projects git wiki.
A user documentation is not available (and not needed, yet). A user documentation is not available (and not needed, yet).
# Docker
To run the docker-compose as expected, you need to take the following steps:
1. Create a database containing docker, using an appropriate Dockerfile, e.g. the following
```
version: '3.3'
services:
postgis:
image: postgis/postgis
restart: always
container_name: postgis-docker
ports:
- 5433:5432
volumes:
- db-volume:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_USER=postgres
networks:
- db-network-bridge
networks:
db-network-bridge:
driver: "bridge"
volumes:
db-volume:
```
This Dockerfile creates a Docker container running postgresql and postgis, creates the default superuser postgres,
creates a named volume for persisting the database and creates a new network bridge, which **must be used by any other
container, which wants to write/read on this database**.
2. Make sure the name of the network bridge above matches the network in the konova docker-compose.yml
3. Get into the running postgis container (`docker exec -it postgis-docker bash`) and create new databases, users and so on. Make sure the database `konova` exists now!
4. Replace all `CHANGE_ME_xy` values inside of konova/docker-compose.yml for your installation. Make sure the `SSO_HOST` holds the proper SSO host, e.g. for the arnova project `arnova.example.org` (Arnova must be installed and the webserver configured as well, of course)
5. Take a look on konova/settings.py and konova/sub_settings/django_settings.py. Again: Replace all occurences of `CHANGE_ME` with proper values for your installation.
1. Make sure you have the proper host strings added to `ALLOWED_HOSTS` inside of django_settings.py.
6. Build and run the docker setup using `docker-compose build` and `docker-compose start` from the main directory of this project (where the docker-compose.yml lives)
7. Run migrations! To do so, get into the konova service container (`docker exec -it konova-docker bash`) and run the needed commands (`python manage.py makemigrations LIST_OF_ALL_MIGRATABLE_APPS`, then `python manage.py migrate`)
8. Run the setup command `python manage.py setup` and follow the instructions on the CLI
9. To enable **SMTP** mail support, make sure your host machine (the one where the docker container run) has the postfix service configured properly. Make sure the `mynetworks` variable is xtended using the docker network bridge ip, created in the postgis container and used by the konova services.
1. **Hint**: You can find out this easily by trying to perform a test mail in the running konova web application (which will fail, of course). Then take a look to the latest entries in `/var/log/mail.log` on your host machine. The failed IP will be displayed there.
2. **Please note**: This installation guide is based on SMTP using postfix!
3. Restart the postfix service on your host machine to reload the new configuration (`service postfix restart`)
10. Finally, make sure your host machine webserver passes incoming requests properly to the docker nginx webserver of konova. A proper nginx config for the host machine may look like this:
```
server {
server_name konova.domain.org;
location / {
proxy_pass http://localhost:KONOVA_NGINX_DOCKER_PORT/;
proxy_set_header Host $host;
}
}
```

View File

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

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

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

0
codelist/views.py Normal file
View File

View File

@@ -168,17 +168,6 @@ class NewCompensationForm(AbstractCompensationForm,
comp.log.add(action) comp.log.add(action)
return comp, 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): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
comp, action = self.__create_comp(user) comp, action = self.__create_comp(user)

View File

@@ -237,11 +237,7 @@ class EditEcoAccountForm(NewEcoAccountForm):
class RemoveEcoAccountModalForm(RemoveModalForm): class RemoveEcoAccountModalForm(RemoveModalForm):
""" Form class
Provides a form for deleting eco accounts
"""
def is_valid(self): def is_valid(self):
super_valid = super().is_valid() super_valid = super().is_valid()
has_deductions = self.instance.deductions.exists() has_deductions = self.instance.deductions.exists()

View File

@@ -7,12 +7,10 @@ Created on: 18.08.22
""" """
from dal import autocomplete from dal import autocomplete
from django import forms from django import forms
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
from compensation.models import CompensationAction
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple
from konova.forms.modals import BaseModalForm, RemoveModalForm from konova.forms.modals import BaseModalForm, RemoveModalForm
from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, ADDED_COMPENSATION_ACTION from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, ADDED_COMPENSATION_ACTION
@@ -116,8 +114,7 @@ class EditCompensationActionModalForm(NewCompensationActionModalForm):
action = None action = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
action_id = kwargs.pop("action_id", None) self.action = kwargs.pop("action", None)
self.action = get_object_or_404(CompensationAction, id=action_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit action") self.form_title = _("Edit action")
form_data = { form_data = {
@@ -150,8 +147,8 @@ class RemoveCompensationActionModalForm(RemoveModalForm):
action = None action = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
action_id = kwargs.pop("action_id", None) action = kwargs.pop("action", None)
self.action = get_object_or_404(CompensationAction, id=action_id) self.action = action
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def save(self): def save(self):

View File

@@ -6,11 +6,10 @@ Created on: 18.08.22
""" """
from django import forms from django import forms
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from konova.forms.modals import BaseModalForm from konova.forms.modals import BaseModalForm
from konova.models import DeadlineType, Deadline from konova.models import DeadlineType
from konova.utils import validators from konova.utils import validators
from konova.utils.message_templates import DEADLINE_EDITED from konova.utils.message_templates import DEADLINE_EDITED
@@ -91,8 +90,7 @@ class EditDeadlineModalForm(NewDeadlineModalForm):
deadline = None deadline = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
deadline_id = kwargs.pop("deadline_id", None) self.deadline = kwargs.pop("deadline", None)
self.deadline = get_object_or_404(Deadline, id=deadline_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit deadline") self.form_title = _("Edit deadline")
form_data = { form_data = {

View File

@@ -6,27 +6,12 @@ Created on: 18.08.22
""" """
from compensation.models import CompensationDocument, EcoAccountDocument from compensation.models import CompensationDocument, EcoAccountDocument
from konova.forms.modals import NewDocumentModalForm, EditDocumentModalForm, RemoveDocumentModalForm from konova.forms.modals import NewDocumentModalForm
class NewCompensationDocumentModalForm(NewDocumentModalForm): class NewCompensationDocumentModalForm(NewDocumentModalForm):
_DOCUMENT_CLS = CompensationDocument document_model = CompensationDocument
class EditCompensationDocumentModalForm(EditDocumentModalForm):
_DOCUMENT_CLS = CompensationDocument
class RemoveCompensationDocumentModalForm(RemoveDocumentModalForm):
_DOCUMENT_CLS = CompensationDocument
class NewEcoAccountDocumentModalForm(NewDocumentModalForm): class NewEcoAccountDocumentModalForm(NewDocumentModalForm):
_DOCUMENT_CLS = EcoAccountDocument document_model = EcoAccountDocument
class EditEcoAccountDocumentModalForm(EditDocumentModalForm):
_DOCUMENT_CLS = EcoAccountDocument
class RemoveEcoAccountDocumentModalForm(RemoveDocumentModalForm):
_DOCUMENT_CLS = EcoAccountDocument

View File

@@ -6,10 +6,8 @@ Created on: 18.08.22
""" """
from django import forms from django import forms
from django.shortcuts import get_object_or_404
from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _ from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _
from compensation.models import Payment
from konova.forms.modals import RemoveModalForm, BaseModalForm from konova.forms.modals import RemoveModalForm, BaseModalForm
from konova.utils import validators from konova.utils import validators
from konova.utils.message_templates import PAYMENT_EDITED from konova.utils.message_templates import PAYMENT_EDITED
@@ -105,8 +103,7 @@ class EditPaymentModalForm(NewPaymentForm):
payment = None payment = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
payment_id = kwargs.pop("payment_id", None) self.payment = kwargs.pop("payment", None)
self.payment = get_object_or_404(Payment, id=payment_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit payment") self.form_title = _("Edit payment")
form_date = { form_date = {
@@ -136,8 +133,8 @@ class RemovePaymentModalForm(RemoveModalForm):
payment = None payment = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
payment_id = kwargs.pop("payment_id", None) payment = kwargs.pop("payment", None)
self.payment = get_object_or_404(Payment, id=payment_id) self.payment = payment
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def save(self): def save(self):

View File

@@ -1,15 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 21.10.25
"""
from compensation.models import Compensation, EcoAccount
from konova.forms.modals import ResubmissionModalForm
class CompensationResubmissionModalForm(ResubmissionModalForm):
_MODEL_CLS = Compensation
class EcoAccountResubmissionModalForm(ResubmissionModalForm):
_MODEL_CLS = EcoAccount

View File

@@ -5,17 +5,21 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22 Created on: 18.08.22
""" """
from bootstrap_modal_forms.mixins import is_ajax
from dal import autocomplete from dal import autocomplete
from django import forms from django import forms
from django.contrib import messages
from django.http import HttpResponseRedirect, HttpRequest
from django.shortcuts import render
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, \ from codelist.settings import CODELIST_BIOTOPES_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from compensation.models import CompensationState
from intervention.inputs import CompensationStateTreeRadioSelect from intervention.inputs import CompensationStateTreeRadioSelect
from konova.contexts import BaseContext
from konova.forms.modals import RemoveModalForm, BaseModalForm from konova.forms.modals import RemoveModalForm, BaseModalForm
from konova.utils.message_templates import COMPENSATION_STATE_EDITED, ADDED_COMPENSATION_STATE from konova.utils.message_templates import COMPENSATION_STATE_EDITED, FORM_INVALID, ADDED_COMPENSATION_STATE
class NewCompensationStateModalForm(BaseModalForm): class NewCompensationStateModalForm(BaseModalForm):
@@ -64,13 +68,10 @@ class NewCompensationStateModalForm(BaseModalForm):
) )
) )
_is_before_state: bool = False
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("New state") self.form_title = _("New state")
self.form_caption = _("Insert data for the new state") self.form_caption = _("Insert data for the new state")
self._is_before_state = bool(self.request.GET.get("before", False))
choices = KonovaCode.objects.filter( choices = KonovaCode.objects.filter(
code_lists__in=[CODELIST_BIOTOPES_ID], code_lists__in=[CODELIST_BIOTOPES_ID],
is_archived=False, is_archived=False,
@@ -82,19 +83,65 @@ class NewCompensationStateModalForm(BaseModalForm):
] ]
self.fields["biotope_type"].choices = choices self.fields["biotope_type"].choices = choices
def save(self): def save(self, is_before_state: bool = False):
state = self.instance.add_state(self, self._is_before_state) state = self.instance.add_state(self, is_before_state)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_STATE) self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_STATE)
return state return state
def process_request(self, request: HttpRequest, msg_success: str = _("Object removed"), msg_error: str = FORM_INVALID, redirect_url: str = None):
""" Generic processing of request
Wraps the request processing logic, so we don't need the same code everywhere a RemoveModalForm is being used
+++
The generic method from super class can not be used, since we need to do some request parameter check in here.
+++
Args:
request (HttpRequest): The incoming request
msg_success (str): The message in case of successful removing
msg_error (str): The message in case of an error
Returns:
"""
redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home")
template = self.template
if request.method == "POST":
if self.is_valid():
# 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.
if not is_ajax(request.META):
is_before_state = bool(request.GET.get("before", False))
self.save(is_before_state=is_before_state)
messages.success(
request,
msg_success
)
return HttpResponseRedirect(redirect_url)
else:
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
elif request.method == "GET":
context = {
"form": self,
}
context = BaseContext(request, context).context
return render(request, template, context)
else:
raise NotImplementedError
class EditCompensationStateModalForm(NewCompensationStateModalForm): class EditCompensationStateModalForm(NewCompensationStateModalForm):
state = None state = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
state_id = kwargs.pop("state_id", None) self.state = kwargs.pop("state", None)
self.state = CompensationState.objects.get(id=state_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit state") self.form_title = _("Edit state")
biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None
@@ -125,8 +172,8 @@ class RemoveCompensationStateModalForm(RemoveModalForm):
state = None state = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
state_id = kwargs.pop("state_id", None) state = kwargs.pop("state", None)
self.state = CompensationState.objects.get(id=state_id) self.state = state
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def save(self): def save(self):

View File

@@ -53,7 +53,7 @@
</td> </td>
<td class="align-middle"> <td class="align-middle">
{% if deduction.intervention.recorded %} {% if deduction.intervention.recorded %}
<em title="{% trans 'Recorded on' %} {{deduction.intervention.recorded.timestamp}} {% trans 'by' %} {{deduction.intervention.recorded.user}}" class='fas fa-bookmark registered-bookmark'></em> <em title="{% trans 'Recorded on' %} {{obj.recorded.timestamp}} {% trans 'by' %} {{obj.recorded.user}}" class='fas fa-bookmark registered-bookmark'></em>
{% else %} {% else %}
<em title="{% trans 'Not recorded yet' %}" class='far fa-bookmark'></em> <em title="{% trans 'Not recorded yet' %}" class='far fa-bookmark'></em>
{% endif %} {% endif %}

View File

@@ -80,11 +80,7 @@ class EditCompensationActionModalFormTestCase(NewCompensationActionModalFormTest
self.compensation.actions.add(self.comp_action) self.compensation.actions.add(self.comp_action)
def test_init(self): def test_init(self):
form = EditCompensationActionModalForm( form = EditCompensationActionModalForm(request=self.request, instance=self.compensation, action=self.comp_action)
request=self.request,
instance=self.compensation,
action_id=self.comp_action.id
)
self.assertEqual(form.form_title, str(_("Edit action"))) self.assertEqual(form.form_title, str(_("Edit action")))
self.assertEqual(len(form.fields["action_type"].initial), self.comp_action.action_type.count()) self.assertEqual(len(form.fields["action_type"].initial), self.comp_action.action_type.count())
self.assertEqual(len(form.fields["action_type_details"].initial), self.comp_action.action_type_details.count()) self.assertEqual(len(form.fields["action_type_details"].initial), self.comp_action.action_type_details.count())
@@ -105,7 +101,7 @@ class EditCompensationActionModalFormTestCase(NewCompensationActionModalFormTest
"comment": comment, "comment": comment,
} }
form = EditCompensationActionModalForm(data, request=self.request, instance=self.compensation, action_id=self.comp_action.id) form = EditCompensationActionModalForm(data, request=self.request, instance=self.compensation, action=self.comp_action)
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
action = form.save() action = form.save()
@@ -130,7 +126,7 @@ class RemoveCompensationActionModalFormTestCase(EditCompensationActionModalFormT
def test_init(self): def test_init(self):
self.assertIn(self.comp_action, self.compensation.actions.all()) self.assertIn(self.comp_action, self.compensation.actions.all())
form = RemoveCompensationActionModalForm(request=self.request, instance=self.compensation, action_id=self.comp_action.id) form = RemoveCompensationActionModalForm(request=self.request, instance=self.compensation, action=self.comp_action)
self.assertEqual(form.action, self.comp_action) self.assertEqual(form.action, self.comp_action)
def test_save(self): def test_save(self):
@@ -141,7 +137,7 @@ class RemoveCompensationActionModalFormTestCase(EditCompensationActionModalFormT
data, data,
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
action_id=self.comp_action.id action=self.comp_action
) )
self.assertTrue(form.is_valid()) self.assertTrue(form.is_valid())
self.assertIn(self.comp_action, self.compensation.actions.all()) self.assertIn(self.comp_action, self.compensation.actions.all())
@@ -190,20 +186,12 @@ class NewCompensationStateModalFormTestCase(BaseTestCase):
self.assertEqual(self.compensation.before_states.count(), 0) self.assertEqual(self.compensation.before_states.count(), 0)
self.assertEqual(self.compensation.after_states.count(), 0) self.assertEqual(self.compensation.after_states.count(), 0)
self.request.GET._mutable = True form = NewCompensationStateModalForm(data, request=self.request, instance=self.compensation)
self.request.GET.update(
{
"before": True,
}
)
self.request.GET._mutable = False
form = NewCompensationStateModalForm(
data,
request=self.request,
instance=self.compensation,
)
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)
state = form.save()
is_before_state = True
state = form.save(is_before_state)
self.assertEqual(self.compensation.before_states.count(), 1) self.assertEqual(self.compensation.before_states.count(), 1)
self.assertEqual(self.compensation.after_states.count(), 0) self.assertEqual(self.compensation.after_states.count(), 0)
@@ -217,16 +205,8 @@ class NewCompensationStateModalFormTestCase(BaseTestCase):
self.assertEqual(last_log.action, UserAction.EDITED) self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, ADDED_COMPENSATION_STATE) self.assertEqual(last_log.comment, ADDED_COMPENSATION_STATE)
self.request.GET._mutable = True is_before_state = False
del self.request.GET["before"] state = form.save(is_before_state)
self.request.GET._mutable = False
form = NewCompensationStateModalForm(
data,
request=self.request,
instance=self.compensation,
)
self.assertTrue(form.is_valid(), msg=form.errors)
state = form.save()
self.assertEqual(self.compensation.before_states.count(), 1) self.assertEqual(self.compensation.before_states.count(), 1)
self.assertEqual(self.compensation.after_states.count(), 1) self.assertEqual(self.compensation.after_states.count(), 1)
@@ -250,11 +230,7 @@ class EditCompensationStateModalFormTestCase(NewCompensationStateModalFormTestCa
self.compensation.after_states.add(self.comp_state) self.compensation.after_states.add(self.comp_state)
def test_init(self): def test_init(self):
form = EditCompensationStateModalForm( form = EditCompensationStateModalForm(request=self.request, instance=self.compensation, state=self.comp_state)
request=self.request,
instance=self.compensation,
state_id=self.comp_state.id
)
self.assertEqual(form.state, self.comp_state) self.assertEqual(form.state, self.comp_state)
self.assertEqual(form.form_title, str(_("Edit state"))) self.assertEqual(form.form_title, str(_("Edit state")))
@@ -285,7 +261,7 @@ class EditCompensationStateModalFormTestCase(NewCompensationStateModalFormTestCa
data, data,
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
state_id=self.comp_state.id state=self.comp_state
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)
@@ -306,11 +282,7 @@ class RemoveCompensationStateModalFormTestCase(EditCompensationStateModalFormTes
super().setUp() super().setUp()
def test_init(self): def test_init(self):
form = RemoveCompensationStateModalForm( form = RemoveCompensationStateModalForm(request=self.request, instance=self.compensation, state=self.comp_state)
request=self.request,
instance=self.compensation,
state_id=self.comp_state.id
)
self.assertEqual(form.state, self.comp_state) self.assertEqual(form.state, self.comp_state)
@@ -322,7 +294,7 @@ class RemoveCompensationStateModalFormTestCase(EditCompensationStateModalFormTes
data, data,
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
state_id=self.comp_state.id state=self.comp_state
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)

View File

@@ -36,7 +36,7 @@ class AbstractCompensationModelTestCase(BaseTestCase):
data, data,
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
deadline_id=self.finished_deadline.id, deadline=self.finished_deadline,
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)
self.assertIn(self.finished_deadline, self.compensation.deadlines.all()) self.assertIn(self.finished_deadline, self.compensation.deadlines.all())

View File

@@ -10,28 +10,27 @@ from django.urls import path
from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \ from compensation.views.compensation.document import EditCompensationDocumentView, NewCompensationDocumentView, \
GetCompensationDocumentView, RemoveCompensationDocumentView GetCompensationDocumentView, RemoveCompensationDocumentView
from compensation.views.compensation.resubmission import CompensationResubmissionView from compensation.views.compensation.resubmission import CompensationResubmissionView
from compensation.views.compensation.report import CompensationReportView from compensation.views.compensation.report import report_view
from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \ from compensation.views.compensation.deadline import NewCompensationDeadlineView, EditCompensationDeadlineView, \
RemoveCompensationDeadlineView RemoveCompensationDeadlineView
from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \ from compensation.views.compensation.action import NewCompensationActionView, EditCompensationActionView, \
RemoveCompensationActionView RemoveCompensationActionView
from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \ from compensation.views.compensation.state import NewCompensationStateView, EditCompensationStateView, \
RemoveCompensationStateView RemoveCompensationStateView
from compensation.views.compensation.compensation import \ from compensation.views.compensation.compensation import index_view, new_view, new_id_view, detail_view, edit_view, \
CompensationIndexView, CompensationIdentifierGeneratorView, CompensationDetailView, \ remove_view
NewCompensationFormView, EditCompensationFormView, RemoveCompensationView
from compensation.views.compensation.log import CompensationLogView from compensation.views.compensation.log import CompensationLogView
urlpatterns = [ urlpatterns = [
# Main compensation # Main compensation
path("", CompensationIndexView.as_view(), name="index"), path("", index_view, name="index"),
path('new/id', CompensationIdentifierGeneratorView.as_view(), name='new-id'), path('new/id', new_id_view, name='new-id'),
path('new/<intervention_id>', NewCompensationFormView.as_view(), name='new'), path('new/<intervention_id>', new_view, name='new'),
path('new', NewCompensationFormView.as_view(), name='new'), path('new', new_view, name='new'),
path('<id>', CompensationDetailView.as_view(), name='detail'), path('<id>', detail_view, name='detail'),
path('<id>/log', CompensationLogView.as_view(), name='log'), path('<id>/log', CompensationLogView.as_view(), name='log'),
path('<id>/edit', EditCompensationFormView.as_view(), name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', RemoveCompensationView.as_view(), name='remove'), path('<id>/remove', remove_view, name='remove'),
path('<id>/state/new', NewCompensationStateView.as_view(), name='new-state'), path('<id>/state/new', NewCompensationStateView.as_view(), name='new-state'),
path('<id>/state/<state_id>/edit', EditCompensationStateView.as_view(), name='state-edit'), path('<id>/state/<state_id>/edit', EditCompensationStateView.as_view(), name='state-edit'),
@@ -44,7 +43,7 @@ urlpatterns = [
path('<id>/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"), path('<id>/deadline/new', NewCompensationDeadlineView.as_view(), name="new-deadline"),
path('<id>/deadline/<deadline_id>/edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'), path('<id>/deadline/<deadline_id>/edit', EditCompensationDeadlineView.as_view(), name='deadline-edit'),
path('<id>/deadline/<deadline_id>/remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'), path('<id>/deadline/<deadline_id>/remove', RemoveCompensationDeadlineView.as_view(), name='deadline-remove'),
path('<id>/report', CompensationReportView.as_view(), name='report'), path('<id>/report', report_view, name='report'),
path('<id>/resub', CompensationResubmissionView.as_view(), name='resubmission-create'), path('<id>/resub', CompensationResubmissionView.as_view(), name='resubmission-create'),
# Documents # Documents

View File

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

View File

@@ -6,11 +6,11 @@ Created on: 24.08.21
""" """
from django.urls import path from django.urls import path
from compensation.views.payment import NewPaymentView, RemovePaymentView, EditPaymentView from compensation.views.payment import *
app_name = "pay" app_name = "pay"
urlpatterns = [ urlpatterns = [
path('<id>/new', NewPaymentView.as_view(), name='new'), path('<id>/new', new_payment_view, name='new'),
path('<id>/remove/<payment_id>', RemovePaymentView.as_view(), name='remove'), path('<id>/remove/<payment_id>', payment_remove_view, name='remove'),
path('<id>/edit/<payment_id>', EditPaymentView.as_view(), name='edit'), path('<id>/edit/<payment_id>', payment_edit_view, name='edit'),
] ]

View File

@@ -5,23 +5,53 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.decorators import method_decorator
from compensation.models import Compensation from compensation.forms.modals.compensation_action import RemoveCompensationActionModalForm, \
EditCompensationActionModalForm, NewCompensationActionModalForm
from compensation.models import Compensation, CompensationAction
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.utils.message_templates import COMPENSATION_ACTION_REMOVED, COMPENSATION_ACTION_EDITED, \
COMPENSATION_ACTION_ADDED
from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \
AbstractRemoveCompensationActionView AbstractRemoveCompensationActionView
_COMPENSATION_DETAIL_URL_NAME = "compensation:detail"
class NewCompensationActionView(AbstractNewCompensationActionView): class NewCompensationActionView(AbstractNewCompensationActionView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditCompensationActionView(AbstractEditCompensationActionView): class EditCompensationActionView(AbstractEditCompensationActionView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveCompensationActionView(AbstractRemoveCompensationActionView): class RemoveCompensationActionView(AbstractRemoveCompensationActionView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -6,166 +6,312 @@ Created on: 19.08.22
""" """
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404, redirect from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.forms.compensation import EditCompensationForm, NewCompensationForm from compensation.forms.compensation import EditCompensationForm, NewCompensationForm
from compensation.models import Compensation from compensation.models import Compensation
from compensation.tables.compensation import CompensationTable from compensation.tables.compensation import CompensationTable
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm from konova.forms.modals import RemoveModalForm
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
RECORDED_BLOCKS_EDIT, PARAMS_INVALID from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.views.identifier import AbstractIdentifierGeneratorView from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
from konova.views.form import AbstractNewGeometryFormView, AbstractEditGeometryFormView RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \
from konova.views.index import AbstractIndexView COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
from konova.views.detail import BaseDetailView
from konova.views.remove import BaseRemoveModalFormView
class CompensationIndexView(LoginRequiredMixin, AbstractIndexView): @login_required
_TAB_TITLE = _("Compensations - Overview") @any_group_check
_INDEX_TABLE_CLS = CompensationTable def index_view(request: HttpRequest):
"""
Renders the index view for compensation
def _get_queryset(self): Args:
qs = Compensation.objects.filter( request (HttpRequest): The incoming request
deleted=None, # only show those which are not deleted individually
intervention__deleted=None, # and don't show the ones whose intervention has been deleted Returns:
).order_by( A rendered view
"-modified__timestamp" """
) template = "generic_index.html"
return qs compensations = Compensation.objects.filter(
deleted=None, # only show those which are not deleted individually
intervention__deleted=None, # and don't show the ones whose intervention has been deleted
).order_by(
"-modified__timestamp"
)
table = CompensationTable(
request=request,
queryset=compensations
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Compensations - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
class NewCompensationFormView(AbstractNewGeometryFormView): @login_required
_FORM_CLS = NewCompensationForm @default_group_required
_MODEL_CLS = Compensation @shared_access_required(Intervention, "intervention_id")
_TEMPLATE = "compensation/form/view.html" def new_view(request: HttpRequest, intervention_id: str = None):
_TAB_TITLE = _("New Compensation") """
_REDIRECT_URL = "compensation:detail" Renders a view for a new compensation creation
def _user_has_shared_access(self, user, **kwargs): Args:
# On a new compensation make sure the intervention (if call came directly through an intervention's detail request (HttpRequest): The incoming request
# 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, **kwargs): Returns:
# 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 template = "compensation/form/view.html"
# Compensations can not exist without an intervention if intervention_id is not None:
intervention_id = kwargs.get("intervention_id", None) try:
if intervention_id: intervention = Intervention.objects.get(id=intervention_id)
try: except ObjectDoesNotExist:
intervention = Intervention.objects.get(id=intervention_id) messages.error(request, PARAMS_INVALID)
if intervention.is_recorded: return redirect("home")
messages.info( if intervention.is_recorded:
request, messages.info(
RECORDED_BLOCKS_EDIT 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(AbstractEditGeometryFormView):
_MODEL_CLS = Compensation
_FORM_CLS = EditCompensationForm
_TEMPLATE = "compensation/form/view.html"
_REDIRECT_URL = "compensation:detail"
def _user_has_permission(self, user, **kwargs):
# User has to be a default user
return user.is_default_user()
class CompensationIdentifierGeneratorView(LoginRequiredMixin, AbstractIdentifierGeneratorView):
_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,
)
return comp
def _get_detail_context(self, obj: Compensation):
""" Generate object specific detail context for view
Args:
obj (): The record
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")
# 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
) )
return redirect("intervention:detail", id=intervention_id)
context = { data_form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
"last_checked": last_checked, geom_form = SimpleGeomForm(request.POST or None, read_only=False)
"last_checked_tooltip": last_checked_tooltip, if request.method == "POST":
"actions": actions, if data_form.is_valid() and geom_form.is_valid():
"before_states": before_states, generated_identifier = data_form.cleaned_data.get("identifier", None)
"after_states": after_states, comp = data_form.save(request.user, geom_form)
"sum_before_states": sum_before_states, if generated_identifier != comp.identifier:
"sum_after_states": sum_after_states, messages.info(
"diff_states": diff_states, request,
"has_finished_deadlines": obj.get_finished_deadlines().exists(), 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",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New compensation"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Provides fetching of free identifiers for e.g. AJAX calls
"""
tmp = Compensation()
identifier = tmp.generate_new_identifier()
while Compensation.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
return JsonResponse(
data={
"gen_data": identifier
} }
return context )
class RemoveCompensationView(LoginRequiredMixin, BaseRemoveModalFormView): @login_required
_MODEL_CLS = Compensation @default_group_required
_FORM_CLS = RemoveModalForm @shared_access_required(Compensation, "id")
_REDIRECT_URL = "compensation:index" def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
# 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 request.method == "POST":
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",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(comp.identifier),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
template = "compensation/detail/compensation/view.html"
comp = get_object_or_404(
Compensation.objects.select_related(
"modified",
"created",
"geometry"
),
id=id,
deleted=None,
intervention__deleted=None,
)
geom_form = SimpleGeomForm(instance=comp)
parcels = comp.get_underlying_parcels()
_user = request.user
is_data_shared = comp.intervention.is_shared_with(_user)
# Order states according to surface
before_states = comp.before_states.all().prefetch_related("biotope_type").order_by("-surface")
after_states = comp.after_states.all().prefetch_related("biotope_type").order_by("-surface")
actions = comp.actions.all().prefetch_related("action_type")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = comp.get_surface_before_states()
sum_after_states = comp.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
request = comp.set_status_messages(request)
last_checked = comp.intervention.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
requesting_user_is_only_shared_user = comp.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = {
"obj": comp,
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"geom_form": geom_form,
"parcels": parcels,
"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, template, context)
@login_required_modal
@login_required
@default_group_required
@shared_access_required(Compensation, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
comp = get_object_or_404(Compensation, id=id)
form = RemoveModalForm(request.POST or None, instance=comp, request=request)
return form.process_request(
request=request,
msg_success=COMPENSATION_REMOVED_TEMPLATE.format(comp.identifier),
redirect_url=reverse("compensation:index"),
)
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()

View File

@@ -5,21 +5,45 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import Compensation from compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.deadline import AbstractRemoveDeadlineView, AbstractEditDeadlineView, AbstractNewDeadlineView from konova.views.deadline import AbstractRemoveDeadlineView, AbstractEditDeadlineView, AbstractNewDeadlineView
_COMPENSATION_DETAIL_URL_NAME = "compensation:detail"
class NewCompensationDeadlineView(AbstractNewDeadlineView): class NewCompensationDeadlineView(AbstractNewDeadlineView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditCompensationDeadlineView(AbstractEditDeadlineView): class EditCompensationDeadlineView(AbstractEditDeadlineView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveCompensationDeadlineView(AbstractRemoveDeadlineView): class RemoveCompensationDeadlineView(AbstractRemoveDeadlineView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = _COMPENSATION_DETAIL_URL_NAME redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,33 +5,62 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from compensation.forms.modals.document import NewCompensationDocumentModalForm, EditCompensationDocumentModalForm, \ from django.contrib.auth.decorators import login_required
RemoveCompensationDocumentModalForm from django.utils.decorators import method_decorator
from compensation.forms.modals.document import NewCompensationDocumentModalForm
from compensation.models import Compensation, CompensationDocument from compensation.models import Compensation, CompensationDocument
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.forms.modals import EditDocumentModalForm
from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \
AbstractEditDocumentView AbstractEditDocumentView
class NewCompensationDocumentView(AbstractNewDocumentView): class NewCompensationDocumentView(AbstractNewDocumentView):
_MODEL_CLS = Compensation model = Compensation
_FORM_CLS = NewCompensationDocumentModalForm form = NewCompensationDocumentModalForm
_REDIRECT_URL = "compensation:detail" redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class GetCompensationDocumentView(AbstractGetDocumentView): class GetCompensationDocumentView(AbstractGetDocumentView):
_MODEL_CLS = Compensation model = Compensation
_DOCUMENT_CLS = CompensationDocument document_model = CompensationDocument
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveCompensationDocumentView(AbstractRemoveDocumentView): class RemoveCompensationDocumentView(AbstractRemoveDocumentView):
_MODEL_CLS = Compensation model = Compensation
_DOCUMENT_CLS = CompensationDocument document_model = CompensationDocument
_FORM_CLS = RemoveCompensationDocumentModalForm
_REDIRECT_URL = "compensation:detail" @method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditCompensationDocumentView(AbstractEditDocumentView): class EditCompensationDocumentView(AbstractEditDocumentView):
_MODEL_CLS = Compensation model = Compensation
_DOCUMENT_CLS = CompensationDocument document_model = CompensationDocument
_FORM_CLS = EditCompensationDocumentModalForm form = EditDocumentModalForm
_REDIRECT_URL = "compensation:detail" redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,11 +5,20 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import Compensation from compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.log import AbstractLogView from konova.views.log import AbstractLogView
class CompensationLogView(LoginRequiredMixin, AbstractLogView): class CompensationLogView(AbstractLogView):
_MODEL_CLS = Compensation model = Compensation
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,48 +5,77 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.models import Compensation from compensation.models import Compensation
from konova.sub_settings.django_settings import BASE_URL from konova.contexts import BaseContext
from konova.utils.qrcode import QrCode from konova.decorators import uuid_required
from konova.views.report import AbstractReportView from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
@uuid_required
def report_view(request: HttpRequest, id: str):
""" Renders the public report view
class BaseCompensationReportView(AbstractReportView): Args:
def _get_compensation_report_context(self, obj): request (HttpRequest): The incoming request
# Order states by surface id (str): The id of the intervention
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 { Returns:
"before_states": before_states,
"after_states": after_states, """
"actions": actions, # Reuse the compensation report template since compensations are structurally identical
template = "compensation/report/compensation/report.html"
comp = get_object_or_404(Compensation, id=id)
tab_title = _("Report {}").format(comp.identifier)
# If intervention is not recorded (yet or currently) we need to render another template without any data
if not comp.is_ready_for_publish():
template = "report/unavailable.html"
context = {
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()
class CompensationReportView(BaseCompensationReportView): qrcode_url = request.build_absolute_uri(reverse("compensation:report", args=(id,)))
_MODEL = Compensation qrcode_img = generate_qr_code(qrcode_url, 10)
_TEMPLATE = "compensation/report/compensation/report.html" qrcode_lanis_url = comp.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
def _get_report_context(self, obj): # Order states by surface
report_url = BASE_URL + reverse("compensation:report", args=(obj.id,)) before_states = comp.before_states.all().order_by("-surface").prefetch_related("biotope_type")
qrcode_report = QrCode(report_url, 10) after_states = comp.after_states.all().order_by("-surface").prefetch_related("biotope_type")
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7) actions = comp.actions.all().prefetch_related("action_type")
report_context = { context = {
"qrcode": { "obj": comp,
"img": qrcode_report.get_img(), "qrcode": {
"url": qrcode_report.get_content(), "img": qrcode_img,
}, "url": qrcode_url,
"qrcode_lanis": { },
"img": qrcode_lanis.get_img(), "qrcode_lanis": {
"url": qrcode_lanis.get_content(), "img": qrcode_img_lanis,
}, "url": qrcode_lanis_url,
"is_entry_shared": False, # disables action buttons during rendering },
"tables_scrollable": False, "is_entry_shared": False, # disables action buttons during rendering
} "before_states": before_states,
report_context.update(self._get_compensation_report_context(obj)) "after_states": after_states,
return report_context "geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)

View File

@@ -5,12 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from compensation.forms.modals.resubmission import CompensationResubmissionModalForm from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import Compensation from compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.resubmission import AbstractResubmissionView from konova.views.resubmission import AbstractResubmissionView
class CompensationResubmissionView(AbstractResubmissionView): class CompensationResubmissionView(AbstractResubmissionView):
_MODEL_CLS = Compensation model = Compensation
_FORM_CLS = CompensationResubmissionModalForm redirect_url_base = "compensation:detail"
_REDIRECT_URL = "compensation:detail" form_action_url_base = "compensation:resubmission-create"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,21 +5,46 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import Compensation from compensation.models import Compensation
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \
AbstractRemoveCompensationStateView AbstractRemoveCompensationStateView
class NewCompensationStateView(AbstractNewCompensationStateView): class NewCompensationStateView(AbstractNewCompensationStateView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = "compensation:detail" redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditCompensationStateView(AbstractEditCompensationStateView): class EditCompensationStateView(AbstractEditCompensationStateView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = "compensation:detail" redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveCompensationStateView(AbstractRemoveCompensationStateView): class RemoveCompensationStateView(AbstractRemoveCompensationStateView):
_MODEL_CLS = Compensation model = Compensation
_REDIRECT_URL = "compensation:detail" redirect_url = "compensation:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(Compensation, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,22 +5,46 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \
AbstractRemoveCompensationActionView AbstractRemoveCompensationActionView
_ECO_ACCOUNT_DETAIL_URL_NAME = "compensation:acc:detail"
class NewEcoAccountActionView(AbstractNewCompensationActionView): class NewEcoAccountActionView(AbstractNewCompensationActionView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditEcoAccountActionView(AbstractEditCompensationActionView): class EditEcoAccountActionView(AbstractEditCompensationActionView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEcoAccountActionView(AbstractRemoveCompensationActionView): class RemoveEcoAccountActionView(AbstractRemoveCompensationActionView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,22 +5,45 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.deadline import AbstractNewDeadlineView, AbstractEditDeadlineView, AbstractRemoveDeadlineView from konova.views.deadline import AbstractNewDeadlineView, AbstractEditDeadlineView, AbstractRemoveDeadlineView
_ECO_ACCOUNT_DETAIL_URL_NAME = "compensation:acc:detail"
class NewEcoAccountDeadlineView(AbstractNewDeadlineView): class NewEcoAccountDeadlineView(AbstractNewDeadlineView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditEcoAccountDeadlineView(AbstractEditDeadlineView): class EditEcoAccountDeadlineView(AbstractEditDeadlineView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEcoAccountDeadlineView(AbstractRemoveDeadlineView): class RemoveEcoAccountDeadlineView(AbstractRemoveDeadlineView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIL_URL_NAME redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,33 +5,54 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.http import Http404 from django.http import Http404
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.decorators import default_group_required, login_required_modal
from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView
_ECO_ACCOUNT_DETAIl_URL_NAME = "compensation:acc:detail"
class NewEcoAccountDeductionView(LoginRequiredMixin, AbstractNewDeductionView): class NewEcoAccountDeductionView(AbstractNewDeductionView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME 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): def _custom_check(self, obj):
# New deductions can only be created if the eco account has been recorded
if not obj.recorded: if not obj.recorded:
raise Http404() raise Http404()
def _check_for_recorded_instance(self, obj):
# Deductions can be created on recorded as well as on non-recorded entries class EditEcoAccountDeductionView(AbstractEditDeductionView):
return None 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 EditEcoAccountDeductionView(LoginRequiredMixin, AbstractEditDeductionView): class RemoveEcoAccountDeductionView(AbstractRemoveDeductionView):
_MODEL_CLS = EcoAccount def _custom_check(self, obj):
_REDIRECT_URL = _ECO_ACCOUNT_DETAIl_URL_NAME 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

View File

@@ -5,33 +5,65 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from compensation.forms.modals.document import NewEcoAccountDocumentModalForm, RemoveEcoAccountDocumentModalForm, \ from django.contrib.auth.decorators import login_required
EditEcoAccountDocumentModalForm from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.utils.decorators import method_decorator
from compensation.forms.modals.document import NewEcoAccountDocumentModalForm
from compensation.models import EcoAccount, EcoAccountDocument from compensation.models import EcoAccount, EcoAccountDocument
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.forms.modals import EditDocumentModalForm
from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \
AbstractEditDocumentView AbstractEditDocumentView
class NewEcoAccountDocumentView(AbstractNewDocumentView): class NewEcoAccountDocumentView(AbstractNewDocumentView):
_MODEL_CLS = EcoAccount model = EcoAccount
_FORM_CLS = NewEcoAccountDocumentModalForm form = NewEcoAccountDocumentModalForm
_REDIRECT_URL = "compensation:acc:detail" redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class GetEcoAccountDocumentView(AbstractGetDocumentView): class GetEcoAccountDocumentView(AbstractGetDocumentView):
_MODEL_CLS = EcoAccount model = EcoAccount
_DOCUMENT_CLS = EcoAccountDocument document_model = EcoAccountDocument
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEcoAccountDocumentView(AbstractRemoveDocumentView): class RemoveEcoAccountDocumentView(AbstractRemoveDocumentView):
_MODEL_CLS = EcoAccount model = EcoAccount
_DOCUMENT_CLS = EcoAccountDocument document_model = EcoAccountDocument
_FORM_CLS = RemoveEcoAccountDocumentModalForm
_REDIRECT_URL = "compensation:acc:detail" @method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditEcoAccountDocumentView(AbstractEditDocumentView): class EditEcoAccountDocumentView(AbstractEditDocumentView):
_MODEL_CLS = EcoAccount model = EcoAccount
_DOCUMENT_CLS = EcoAccountDocument document_model = EcoAccountDocument
_FORM_CLS = EditEcoAccountDocumentModalForm form = EditDocumentModalForm
_REDIRECT_URL = "compensation:acc:detail" redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,159 +5,303 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib import messages
from django.shortcuts import get_object_or_404 from django.contrib.auth.decorators import login_required
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm, RemoveEcoAccountModalForm from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm, RemoveEcoAccountModalForm
from compensation.models import EcoAccount from compensation.models import EcoAccount
from compensation.tables.eco_account import EcoAccountTable from compensation.tables.eco_account import EcoAccountTable
from konova.views.identifier import AbstractIdentifierGeneratorView from konova.contexts import BaseContext
from konova.views.form import AbstractNewGeometryFormView, AbstractEditGeometryFormView from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
from konova.views.index import AbstractIndexView uuid_required
from konova.views.detail import BaseDetailView from konova.forms import SimpleGeomForm
from konova.views.remove import BaseRemoveModalFormView from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
class EcoAccountIndexView(LoginRequiredMixin, AbstractIndexView): @login_required
""" View class for indexing eco accounts @any_group_check
def index_view(request: HttpRequest):
"""
Renders the index view for eco accounts
Args:
request (HttpRequest): The incoming request
Returns:
A rendered view
"""
template = "generic_index.html"
eco_accounts = EcoAccount.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
table = EcoAccountTable(
request=request,
queryset=eco_accounts
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Eco-account - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required
@default_group_required
def new_view(request: HttpRequest):
"""
Renders a view for a new eco account creation
Args:
request (HttpRequest): The incoming request
Returns:
""" """
_INDEX_TABLE_CLS = EcoAccountTable template = "compensation/form/view.html"
_TAB_TITLE = _("Eco-account - Overview") data_form = NewEcoAccountForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
acc = data_form.save(request.user, geom_form)
if generated_identifier != acc.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
acc.identifier
)
)
messages.success(request, _("Eco-Account {} added").format(acc.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
def _get_queryset(self): num_ignored_geometries = geom_form.get_num_geometries_ignored()
qs = EcoAccount.objects.filter( if num_ignored_geometries > 0:
deleted=None, messages.info(
).order_by( request,
"-modified__timestamp" GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
) )
return qs
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)
class NewEcoAccountFormView(AbstractNewGeometryFormView): @login_required
""" Form view class @default_group_required
def new_id_view(request: HttpRequest):
""" JSON endpoint
Renders a form for new eco accounts Provides fetching of free identifiers for e.g. AJAX calls
""" """
_FORM_CLS = NewEcoAccountForm tmp = EcoAccount()
_MODEL_CLS = EcoAccount identifier = tmp.generate_new_identifier()
_TEMPLATE = "compensation/form/view.html" while EcoAccount.objects.filter(identifier=identifier).exists():
_TAB_TITLE = _("New Eco-Account") identifier = tmp.generate_new_identifier()
_REDIRECT_URL = "compensation:acc:detail" return JsonResponse(
data={
def _user_has_permission(self, user, **kwargs): "gen_data": identifier
# User has to be a default user
return user.is_default_user()
class EditEcoAccountFormView(AbstractEditGeometryFormView):
""" Form view class
Renders a form for editing of eco accounts
"""
_FORM_CLS = EditEcoAccountForm
_MODEL_CLS = EcoAccount
_TEMPLATE = "compensation/form/view.html"
_REDIRECT_URL = "compensation:acc:detail"
def _user_has_permission(self, user, **kwargs):
# User has to be a default user
return user.is_default_user()
class EcoAccountIdentifierGeneratorView(LoginRequiredMixin, AbstractIdentifierGeneratorView):
""" View class for identifier generation on eco accounts
"""
_MODEL_CLS = EcoAccount
_REDIRECT_URL = "compensation:acc:index"
class EcoAccountDetailView(BaseDetailView):
""" Detail view class
Renders details of an eco account
"""
_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()
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(),
} }
return context )
class RemoveEcoAccountView(LoginRequiredMixin, BaseRemoveModalFormView): @login_required
""" Form view class @default_group_required
@shared_access_required(EcoAccount, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
Renders a form for removing eco accounts Args:
request (HttpRequest): The incoming request
Returns:
""" """
_MODEL_CLS = EcoAccount template = "compensation/form/view.html"
_FORM_CLS = RemoveEcoAccountModalForm # Get object from db
_REDIRECT_URL = "compensation:acc:index" 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)
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:
# The data form takes the geom form for processing, as well as the performing user
acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("compensation:acc:detail", id=acc.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
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)
@login_required
@any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation
Args:
request (HttpRequest): The incoming request
id (str): The compensation's id
Returns:
"""
template = "compensation/detail/eco_account/view.html"
acc = get_object_or_404(
EcoAccount.objects.prefetch_related(
"deadlines",
).select_related(
'geometry',
'responsible',
),
id=id,
deleted=None,
)
geom_form = SimpleGeomForm(instance=acc)
parcels = acc.get_underlying_parcels()
_user = request.user
is_data_shared = acc.is_shared_with(_user)
# Order states according to surface
before_states = acc.before_states.order_by("-surface")
after_states = acc.after_states.order_by("-surface")
# Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = acc.get_surface_before_states()
sum_after_states = acc.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states)
# Calculate rest of available surface for deductions
available_total = acc.deductable_rest
available_relative = acc.get_deductable_rest_relative()
# Prefetch related data to decrease the amount of db connections
deductions = acc.deductions.filter(
intervention__deleted=None,
)
actions = acc.actions.all()
request = acc.set_status_messages(request)
requesting_user_is_only_shared_user = acc.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = {
"obj": acc,
"geom_form": geom_form,
"parcels": parcels,
"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, template, context)
@login_required_modal
@login_required
@default_group_required
@shared_access_required(EcoAccount, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the eco account
Args:
request (HttpRequest): The incoming request
id (str): The account's id
Returns:
"""
acc = get_object_or_404(EcoAccount, id=id)
# If the eco account has already been recorded OR there are already deductions, it can not be deleted by a regular
# default group user
if acc.recorded is not None or acc.deductions.exists():
user = request.user
if not user.in_group(ETS_GROUP):
messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED)
return redirect("compensation:acc:detail", id=id)
form = RemoveEcoAccountModalForm(request.POST or None, instance=acc, request=request)
return form.process_request(
request=request,
msg_success=_("Eco-account removed"),
redirect_url=reverse("compensation:acc:index"),
)
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()

View File

@@ -5,11 +5,20 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.log import AbstractLogView from konova.views.log import AbstractLogView
class EcoAccountLogView(LoginRequiredMixin, AbstractLogView): class EcoAccountLogView(AbstractLogView):
_MODEL_CLS = EcoAccount model = EcoAccount
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,12 +5,20 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.record import AbstractRecordView from konova.views.record import AbstractRecordView
class EcoAccountRecordView(LoginRequiredMixin, AbstractRecordView): class EcoAccountRecordView(AbstractRecordView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,41 +5,85 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount from compensation.models import EcoAccount
from compensation.views.compensation.report import BaseCompensationReportView from konova.contexts import BaseContext
from konova.sub_settings.django_settings import BASE_URL from konova.decorators import uuid_required
from konova.utils.qrcode import QrCode from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
class EcoAccountReportView(BaseCompensationReportView): @uuid_required
_MODEL = EcoAccount def report_view(request: HttpRequest, id: str):
_TEMPLATE = "compensation/report/eco_account/report.html" """ Renders the public report view
def _get_report_context(self, obj): Args:
report_url = BASE_URL + reverse("compensation:acc:report", args=(obj.id,)) request (HttpRequest): The incoming request
qrcode_report = QrCode(report_url, 10) id (str): The id of the intervention
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier) Returns:
deductions = obj.deductions.all() \
.distinct("intervention") \
.select_related("intervention") \
.values_list("intervention__id", "intervention__identifier", "intervention__title", named=True)
report_context = { """
"qrcode": { # Reuse the compensation report template since EcoAccounts are structurally identical
"img": qrcode_report.get_img(), template = "compensation/report/eco_account/report.html"
"url": qrcode_report.get_content(), acc = get_object_or_404(EcoAccount, id=id)
},
"qrcode_lanis": { tab_title = _("Report {}").format(acc.identifier)
"img": qrcode_lanis.get_img(), # If intervention is not recorded (yet or currently) we need to render another template without any data
"url": qrcode_lanis.get_content(), if not acc.is_ready_for_publish():
}, template = "report/unavailable.html"
"is_entry_shared": False, # disables action buttons during rendering context = {
"deductions": deductions, TAB_TITLE_IDENTIFIER: tab_title,
"tables_scrollable": False,
} }
report_context.update(self._get_compensation_report_context(obj)) context = BaseContext(request, context).context
return report_context return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=acc
)
parcels = acc.get_underlying_parcels()
qrcode_url = request.build_absolute_uri(reverse("compensation:acc:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = acc.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
# Order states by surface
before_states = acc.before_states.all().order_by("-surface").select_related("biotope_type__parent")
after_states = acc.after_states.all().order_by("-surface").select_related("biotope_type__parent")
actions = acc.actions.all().prefetch_related("action_type__parent")
# Reduce amount of db fetched data to the bare minimum we need in the template (deduction's intervention id and identifier)
deductions = acc.deductions.all()\
.distinct("intervention")\
.select_related("intervention")\
.values_list("intervention__id", "intervention__identifier", "intervention__title", named=True)
context = {
"obj": acc,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"deductions": deductions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)

View File

@@ -5,12 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from compensation.forms.modals.resubmission import EcoAccountResubmissionModalForm from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.resubmission import AbstractResubmissionView from konova.views.resubmission import AbstractResubmissionView
class EcoAccountResubmissionView(AbstractResubmissionView): class EcoAccountResubmissionView(AbstractResubmissionView):
_MODEL_CLS = EcoAccount model = EcoAccount
_FORM_CLS = EcoAccountResubmissionModalForm redirect_url_base = "compensation:acc:detail"
_REDIRECT_URL = "compensation:acc:detail" form_action_url_base = "compensation:acc:resubmission-create"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,15 +5,29 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.share import AbstractShareByTokenView, AbstractShareFormView from konova.views.share import AbstractShareByTokenView, AbstractShareFormView
class EcoAccountShareByTokenView(AbstractShareByTokenView): class EcoAccountShareByTokenView(AbstractShareByTokenView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = "compensation:acc:detail" redirect_url = "compensation:acc:detail"
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EcoAccountShareFormView(AbstractShareFormView): class EcoAccountShareFormView(AbstractShareFormView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,21 +5,46 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from compensation.models import EcoAccount from compensation.models import EcoAccount
from konova.decorators import shared_access_required, default_group_required, login_required_modal
from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \
AbstractRemoveCompensationStateView AbstractRemoveCompensationStateView
class NewEcoAccountStateView(AbstractNewCompensationStateView): class NewEcoAccountStateView(AbstractNewCompensationStateView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = "compensation:acc:detail" redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EditEcoAccountStateView(AbstractEditCompensationStateView): class EditEcoAccountStateView(AbstractEditCompensationStateView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = "compensation:acc:detail" redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class RemoveEcoAccountStateView(AbstractRemoveCompensationStateView): class RemoveEcoAccountStateView(AbstractRemoveCompensationStateView):
_MODEL_CLS = EcoAccount model = EcoAccount
_REDIRECT_URL = "compensation:acc:detail" redirect_url = "compensation:acc:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(default_group_required)
@method_decorator(shared_access_required(EcoAccount, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,38 +5,84 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 09.08.21 Created on: 09.08.21
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.urls import reverse
from django.contrib.auth.decorators import login_required
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from compensation.forms.modals.payment import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm from compensation.forms.modals.payment import NewPaymentForm, RemovePaymentModalForm, EditPaymentModalForm
from compensation.models import Payment
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import default_group_required, shared_access_required
from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED from konova.utils.message_templates import PAYMENT_ADDED, PAYMENT_REMOVED, PAYMENT_EDITED
from konova.views.modal import AbstractModalFormView
class BasePaymentView(LoginRequiredMixin, AbstractModalFormView): @login_required
_MODEL_CLS = Intervention @default_group_required
_REDIRECT_URL = "intervention:detail" @shared_access_required(Intervention, "id")
def new_payment_view(request: HttpRequest, id: str):
""" Renders a modal view for adding new payments
class Meta: Args:
abstract = True request (HttpRequest): The incoming request
id (str): The intervention's id for which a new payment shall be added
def _get_redirect_url(self, *args, **kwargs): Returns:
url = super()._get_redirect_url(*args, **kwargs)
return f"{url}#related_data"
def _user_has_permission(self, user, **kwargs): """
return user.is_default_user() 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"
)
class NewPaymentView(BasePaymentView): @login_required
_FORM_CLS = NewPaymentForm @default_group_required
_MSG_SUCCESS = PAYMENT_ADDED @shared_access_required(Intervention, "id")
def payment_remove_view(request: HttpRequest, id: str, payment_id: str):
""" Renders a modal view for removing payments
Args:
request (HttpRequest): The incoming request
id (str): The intervention's id
payment_id (str): The payment's id
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
payment = get_object_or_404(Payment, id=payment_id)
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"
)
class EditPaymentView(BasePaymentView): @login_required
_MSG_SUCCESS = PAYMENT_EDITED @default_group_required
_FORM_CLS = EditPaymentModalForm @shared_access_required(Intervention, "id")
def payment_edit_view(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
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"
)
class RemovePaymentView(BasePaymentView):
_MSG_SUCCESS = PAYMENT_REMOVED
_FORM_CLS = RemovePaymentModalForm

23
docker-compose.yml Normal file
View File

@@ -0,0 +1,23 @@
version: '3.3'
services:
konova:
external_links:
- postgis:db
- arnova-nginx-server:arnova
build: .
image: "ksp/konova:1.8"
container_name: "konova-docker"
command: ./docker-entrypoint.sh
restart: always
volumes:
- /data/apps/konova/uploaded_files:/konova_uploaded_files
ports:
- "1337:80"
# Instead of an own, new network, we need to connect to the existing one, which is provided by the postgis container
# NOTE: THIS NETWORK MUST EXIST
networks:
default:
external:
name: postgis_nat_it_backend

27
docker-entrypoint.sh Executable file
View File

@@ -0,0 +1,27 @@
#!/bin/bash
set -e # Beende Skript bei Fehlern
set -o pipefail # Fehler in Pipelines nicht ignorieren
# Starte Redis
redis-server --daemonize yes
# Starte Celery Worker im Hintergrund
celery -A konova worker --loglevel=info &
# Starte Nginx als Hintergrundprozess
nginx -g "daemon off;" &
# Setze Gunicorn Worker-Anzahl (Standard: (2*CPUs)+1)
WORKERS=${GUNICORN_WORKERS:-$((2 * $(nproc) + 1))}
# Stelle sicher, dass Logs existieren
mkdir -p /var/log/gunicorn
touch /var/log/gunicorn/access.log /var/log/gunicorn/error.log
# Starte Gunicorn als Hauptprozess
exec gunicorn --workers="$WORKERS" konova.wsgi:application \
--bind=0.0.0.0:8000 \
--access-logfile /var/log/gunicorn/access.log \
--error-logfile /var/log/gunicorn/error.log \
--access-logformat '%({x-real-ip}i)s via %(h)s %(l)s %(u)s %(t)s "%(r)s" %(s)s %(b)s "%(f)s" "%(a)s"'

View File

@@ -15,8 +15,7 @@ from compensation.forms.compensation import AbstractCompensationForm
from ema.models import Ema, EmaDocument from ema.models import Ema, EmaDocument
from intervention.models import Responsibility, Handler from intervention.models import Responsibility, Handler
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import NewDocumentModalForm, EditDocumentModalForm, RemoveDocumentModalForm, \ from konova.forms.modals import NewDocumentModalForm
ResubmissionModalForm
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -171,13 +170,4 @@ class EditEmaForm(NewEmaForm):
class NewEmaDocumentModalForm(NewDocumentModalForm): class NewEmaDocumentModalForm(NewDocumentModalForm):
_DOCUMENT_CLS = EmaDocument document_model = EmaDocument
class EditEmaDocumentModalForm(EditDocumentModalForm):
_DOCUMENT_CLS = EmaDocument
class RemoveEmaDocumentModalForm(RemoveDocumentModalForm):
_DOCUMENT_CLS = EmaDocument
class EmaResubmissionModalForm(ResubmissionModalForm):
_MODEL_CLS = Ema

View File

@@ -10,26 +10,25 @@ from django.urls import path
from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView from ema.views.action import NewEmaActionView, EditEmaActionView, RemoveEmaActionView
from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView from ema.views.deadline import NewEmaDeadlineView, EditEmaDeadlineView, RemoveEmaDeadlineView
from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView from ema.views.document import NewEmaDocumentView, EditEmaDocumentView, RemoveEmaDocumentView, GetEmaDocumentView
from ema.views.ema import EmaIndexView, EmaIdentifierGeneratorView, EmaDetailView, EditEmaFormView, NewEmaFormView, \ from ema.views.ema import index_view, new_view, new_id_view, detail_view, edit_view, remove_view
RemoveEmaView
from ema.views.log import EmaLogView from ema.views.log import EmaLogView
from ema.views.record import EmaRecordView from ema.views.record import EmaRecordView
from ema.views.report import EmaReportView from ema.views.report import report_view
from ema.views.resubmission import EmaResubmissionView from ema.views.resubmission import EmaResubmissionView
from ema.views.share import EmaShareFormView, EmaShareByTokenView from ema.views.share import EmaShareFormView, EmaShareByTokenView
from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView from ema.views.state import NewEmaStateView, EditEmaStateView, RemoveEmaStateView
app_name = "ema" app_name = "ema"
urlpatterns = [ urlpatterns = [
path("", EmaIndexView.as_view(), name="index"), path("", index_view, name="index"),
path("new/", NewEmaFormView.as_view(), name="new"), path("new/", new_view, name="new"),
path("new/id", EmaIdentifierGeneratorView.as_view(), name="new-id"), path("new/id", new_id_view, name="new-id"),
path("<id>", EmaDetailView.as_view(), name="detail"), path("<id>", detail_view, name="detail"),
path('<id>/log', EmaLogView.as_view(), name='log'), path('<id>/log', EmaLogView.as_view(), name='log'),
path('<id>/edit', EditEmaFormView.as_view(), name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', RemoveEmaView.as_view(), name='remove'), path('<id>/remove', remove_view, name='remove'),
path('<id>/record', EmaRecordView.as_view(), name='record'), path('<id>/record', EmaRecordView.as_view(), name='record'),
path('<id>/report', EmaReportView.as_view(), name='report'), path('<id>/report', report_view, name='report'),
path('<id>/resub', EmaResubmissionView.as_view(), name='resubmission-create'), path('<id>/resub', EmaResubmissionView.as_view(), name='resubmission-create'),
path('<id>/state/new', NewEmaStateView.as_view(), name='new-state'), path('<id>/state/new', NewEmaStateView.as_view(), name='new-state'),

View File

@@ -5,31 +5,46 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema from ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \ from konova.views.action import AbstractNewCompensationActionView, AbstractEditCompensationActionView, \
AbstractRemoveCompensationActionView AbstractRemoveCompensationActionView
_EMA_ACCOUNT_DETAIL_URL_NAME = "ema:detail"
class NewEmaActionView(AbstractNewCompensationActionView): class NewEmaActionView(AbstractNewCompensationActionView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME redirect_url = "ema:detail"
@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)
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
class EditEmaActionView(AbstractEditCompensationActionView): class EditEmaActionView(AbstractEditCompensationActionView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME redirect_url = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @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)
class RemoveEmaActionView(AbstractRemoveCompensationActionView): class RemoveEmaActionView(AbstractRemoveCompensationActionView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = _EMA_ACCOUNT_DETAIL_URL_NAME redirect_url = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,30 +5,46 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema from ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.deadline import AbstractNewDeadlineView, AbstractRemoveDeadlineView, AbstractEditDeadlineView from konova.views.deadline import AbstractNewDeadlineView, AbstractRemoveDeadlineView, AbstractEditDeadlineView
_EMA_DETAIL_URL_NAME = "ema:detail"
class NewEmaDeadlineView(AbstractNewDeadlineView): class NewEmaDeadlineView(AbstractNewDeadlineView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = _EMA_DETAIL_URL_NAME redirect_url = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @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)
class EditEmaDeadlineView(AbstractEditDeadlineView): class EditEmaDeadlineView(AbstractEditDeadlineView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = _EMA_DETAIL_URL_NAME redirect_url = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @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)
class RemoveEmaDeadlineView(AbstractRemoveDeadlineView): class RemoveEmaDeadlineView(AbstractRemoveDeadlineView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = _EMA_DETAIL_URL_NAME redirect_url = "ema:detail"
@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)
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()

View File

@@ -5,41 +5,62 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from ema.forms import NewEmaDocumentModalForm, RemoveEmaDocumentModalForm, EditEmaDocumentModalForm from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.forms import NewEmaDocumentModalForm
from ema.models import Ema, EmaDocument from ema.models import Ema, EmaDocument
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.forms.modals import EditDocumentModalForm
from konova.views.document import AbstractEditDocumentView, AbstractRemoveDocumentView, AbstractGetDocumentView, \ from konova.views.document import AbstractEditDocumentView, AbstractRemoveDocumentView, AbstractGetDocumentView, \
AbstractNewDocumentView AbstractNewDocumentView
class NewEmaDocumentView(AbstractNewDocumentView): class NewEmaDocumentView(AbstractNewDocumentView):
_MODEL_CLS = Ema model = Ema
_FORM_CLS = NewEmaDocumentModalForm form = NewEmaDocumentModalForm
_REDIRECT_URL = "ema:detail" redirect_url = "ema:detail"
@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)
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
class GetEmaDocumentView(AbstractGetDocumentView): class GetEmaDocumentView(AbstractGetDocumentView):
_MODEL_CLS = Ema model = Ema
_DOCUMENT_CLS = EmaDocument document_model = EmaDocument
@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)
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
class RemoveEmaDocumentView(AbstractRemoveDocumentView): class RemoveEmaDocumentView(AbstractRemoveDocumentView):
_MODEL_CLS = Ema model = Ema
_DOCUMENT_CLS = EmaDocument document_model = EmaDocument
_FORM_CLS = RemoveEmaDocumentModalForm
_REDIRECT_URL = "ema:detail" @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)
def _user_has_permission(self, user, **kwargs):
return user.is_ets_user()
class EditEmaDocumentView(AbstractEditDocumentView): class EditEmaDocumentView(AbstractEditDocumentView):
_MODEL_CLS = Ema model = Ema
_FORM_CLS = EditEmaDocumentModalForm document_model = EmaDocument
_DOCUMENT_CLS = EmaDocument form = EditDocumentModalForm
_REDIRECT_URL = "ema:detail" redirect_url = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,113 +5,269 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib import messages
from django.shortcuts import get_object_or_404 from django.contrib.auth.decorators import login_required
from django.db.models import Sum
from django.http import HttpRequest, JsonResponse
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from ema.forms import NewEmaForm, EditEmaForm from ema.forms import NewEmaForm, EditEmaForm
from ema.models import Ema from ema.models import Ema
from ema.tables import EmaTable from ema.tables import EmaTable
from konova.views.identifier import AbstractIdentifierGeneratorView from konova.contexts import BaseContext
from konova.views.form import AbstractNewGeometryFormView, AbstractEditGeometryFormView from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal, \
from konova.views.index import AbstractIndexView uuid_required
from konova.views.detail import BaseDetailView from konova.forms import SimpleGeomForm
from konova.views.remove import BaseRemoveModalFormView from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
class EmaIndexView(LoginRequiredMixin, AbstractIndexView): @login_required
_TAB_TITLE = _("EMAs - Overview") def index_view(request: HttpRequest):
_INDEX_TABLE_CLS = EmaTable """ Renders the index view for EMAs
def _get_queryset(self): Args:
qs = Ema.objects.filter( request (HttpRequest): The incoming request
deleted=None,
).order_by( Returns:
"-modified__timestamp"
) """
return qs template = "generic_index.html"
emas = Ema.objects.filter(
deleted=None,
).order_by(
"-modified__timestamp"
)
table = EmaTable(
request,
queryset=emas
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("EMAs - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
class NewEmaFormView(AbstractNewGeometryFormView): @login_required
_FORM_CLS = NewEmaForm @conservation_office_group_required
_MODEL_CLS = Ema def new_view(request: HttpRequest):
_TEMPLATE = "ema/form/view.html" """
_TAB_TITLE = _("New EMA") Renders a view for a new eco account creation
_REDIRECT_URL = "ema:detail"
def _user_has_permission(self, user, **kwargs): Args:
# User has to be an ets user request (HttpRequest): The incoming request
return user.is_ets_user()
Returns:
"""
template = "ema/form/view.html"
data_form = NewEmaForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
ema = data_form.save(request.user, geom_form)
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",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New EMA"),
}
context = BaseContext(request, context).context
return render(request, template, context)
class EditEmaFormView(AbstractEditGeometryFormView): @login_required
_MODEL_CLS = Ema @conservation_office_group_required
_FORM_CLS = EditEmaForm def new_id_view(request: HttpRequest):
_TEMPLATE = "ema/form/view.html" """ JSON endpoint
_REDIRECT_URL = "ema:detail"
_TAB_TITLE = _("Edit {}")
def _user_has_permission(self, user, **kwargs): Provides fetching of free identifiers for e.g. AJAX calls
# User has to be an ets user
return user.is_ets_user()
"""
class EmaIdentifierGeneratorView(LoginRequiredMixin, AbstractIdentifierGeneratorView): tmp = Ema()
_MODEL_CLS = Ema identifier = tmp.generate_new_identifier()
_REDIRECT_URL = "ema:index" while Ema.objects.filter(identifier=identifier).exists():
identifier = tmp.generate_new_identifier()
def _user_has_permission(self, user, **kwargs): return JsonResponse(
return user.is_ets_user() data={
"gen_data": identifier
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)
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(),
} }
return context )
class RemoveEmaView(LoginRequiredMixin, BaseRemoveModalFormView):
_MODEL_CLS = Ema
_REDIRECT_URL = "ema:index"
def _user_has_permission(self, user, **kwargs): @login_required
return user.is_ets_user() @uuid_required
def detail_view(request: HttpRequest, id: str):
""" Renders the detail view of an EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA id
Returns:
"""
template = "ema/detail/view.html"
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, template, context)
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def edit_view(request: HttpRequest, id: str):
"""
Renders a view for editing compensations
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "compensation/form/view.html"
# Get object from db
ema = get_object_or_404(Ema, id=id)
if ema.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect("ema:detail", id=id)
# Create forms, initialize with values from db/from POST request
data_form = EditEmaForm(request.POST or None, instance=ema)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=ema)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user
ema = data_form.save(request.user, geom_form)
messages.success(request, _("EMA {} edited").format(ema.identifier))
if geom_form.has_geometry_simplified():
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",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(ema.identifier),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required_modal
@login_required
@conservation_office_group_required
@shared_access_required(Ema, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a modal view for removing the EMA
Args:
request (HttpRequest): The incoming request
id (str): The EMA's id
Returns:
"""
ema = get_object_or_404(Ema, id=id)
form = RemoveModalForm(request.POST or None, instance=ema, request=request)
return form.process_request(
request=request,
msg_success=_("EMA removed"),
redirect_url=reverse("ema:index"),
)

View File

@@ -5,14 +5,20 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema from ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.log import AbstractLogView from konova.views.log import AbstractLogView
class EmaLogView(LoginRequiredMixin, AbstractLogView): class EmaLogView(AbstractLogView):
_MODEL_CLS = Ema model = Ema
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,12 +5,20 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema from ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.record import AbstractRecordView from konova.views.record import AbstractRecordView
class EmaRecordView(LoginRequiredMixin, AbstractRecordView): class EmaRecordView(AbstractRecordView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = "ema:detail"
@method_decorator(login_required_modal)
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,36 +5,77 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from compensation.views.compensation.report import BaseCompensationReportView
from ema.models import Ema from ema.models import Ema
from konova.sub_settings.django_settings import BASE_URL from konova.contexts import BaseContext
from konova.utils.qrcode import QrCode from konova.decorators import uuid_required
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
@uuid_required
def report_view(request:HttpRequest, id: str):
""" Renders the public report view
class EmaReportView(BaseCompensationReportView): Args:
_TEMPLATE = "ema/report/report.html" request (HttpRequest): The incoming request
_MODEL = Ema id (str): The id of the intervention
def _get_report_context(self, obj): Returns:
report_url = BASE_URL + reverse("ema:report", args=(obj.id,))
qrcode_report = QrCode(report_url, 10)
qrcode_lanis = QrCode(obj.get_LANIS_link(), 7)
generic_compensation_report_context = self._get_compensation_report_context(obj) """
# Reuse the compensation report template since EMAs are structurally identical
template = "ema/report/report.html"
ema = get_object_or_404(Ema, id=id)
report_context = { tab_title = _("Report {}").format(ema.identifier)
"qrcode": { # If intervention is not recorded (yet or currently) we need to render another template without any data
"img": qrcode_report.get_img(), if not ema.is_ready_for_publish():
"url": qrcode_report.get_content(), template = "report/unavailable.html"
}, context = {
"qrcode_lanis": { TAB_TITLE_IDENTIFIER: tab_title,
"img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(),
},
"is_entry_shared": False, # disables action buttons during rendering
"tables_scrollable": False,
} }
report_context.update(generic_compensation_report_context) context = BaseContext(request, context).context
return report_context return render(request, template, context)
# Prepare data for map viewer
geom_form = SimpleGeomForm(
instance=ema,
)
parcels = ema.get_underlying_parcels()
qrcode_url = request.build_absolute_uri(reverse("ema:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = ema.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
# Order states by surface
before_states = ema.before_states.all().order_by("-surface").prefetch_related("biotope_type")
after_states = ema.after_states.all().order_by("-surface").prefetch_related("biotope_type")
actions = ema.actions.all().prefetch_related("action_type")
context = {
"obj": ema,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url
},
"is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,
"parcels": parcels,
"actions": actions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)

View File

@@ -5,16 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from ema.forms import EmaResubmissionModalForm from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema from ema.models import Ema
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal
from konova.views.resubmission import AbstractResubmissionView from konova.views.resubmission import AbstractResubmissionView
class EmaResubmissionView(AbstractResubmissionView): class EmaResubmissionView(AbstractResubmissionView):
_MODEL_CLS = Ema model = Ema
_FORM_CLS = EmaResubmissionModalForm redirect_url_base = "ema:detail"
_REDIRECT_URL = "ema:detail" form_action_url_base = "ema:resubmission-create"
action_url = "ema:resubmission-create"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,17 +5,29 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema from ema.models import Ema
from konova.decorators import conservation_office_group_required, shared_access_required, login_required_modal
from konova.views.share import AbstractShareByTokenView, AbstractShareFormView from konova.views.share import AbstractShareByTokenView, AbstractShareFormView
class EmaShareByTokenView(AbstractShareByTokenView): class EmaShareByTokenView(AbstractShareByTokenView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = "ema:detail" redirect_url = "ema:detail"
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class EmaShareFormView(AbstractShareFormView): class EmaShareFormView(AbstractShareFormView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,30 +5,46 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from ema.models import Ema from ema.models import Ema
from konova.decorators import conservation_office_group_required, shared_access_required, login_required_modal
from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \ from konova.views.state import AbstractNewCompensationStateView, AbstractEditCompensationStateView, \
AbstractRemoveCompensationStateView AbstractRemoveCompensationStateView
class NewEmaStateView(AbstractNewCompensationStateView): class NewEmaStateView(AbstractNewCompensationStateView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = "ema:detail" redirect_url = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @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)
class EditEmaStateView(AbstractEditCompensationStateView): class EditEmaStateView(AbstractEditCompensationStateView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = "ema:detail" redirect_url = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @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)
class RemoveEmaStateView(AbstractRemoveCompensationStateView): class RemoveEmaStateView(AbstractRemoveCompensationStateView):
_MODEL_CLS = Ema model = Ema
_REDIRECT_URL = "ema:detail" redirect_url = "ema:detail"
def _user_has_permission(self, user, **kwargs): @method_decorator(login_required_modal)
return user.is_ets_user() @method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Ema, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -172,8 +172,7 @@ class EditEcoAccountDeductionModalForm(NewEcoAccountDeductionModalForm):
deduction = None deduction = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
deduction_id = kwargs.pop("deduction_id", None) self.deduction = kwargs.pop("deduction", None)
self.deduction = EcoAccountDeduction.objects.get(id=deduction_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit Deduction") self.form_title = _("Edit Deduction")
form_data = { 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. 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): def __init__(self, *args, **kwargs):
deduction_id = kwargs.pop("deduction_id", None) deduction = kwargs.pop("deduction", None)
deduction = EcoAccountDeduction.objects.get(id=deduction_id) self.deduction = deduction
self._DEDUCTION_OBJ = deduction
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def save(self): def save(self):
with transaction.atomic(): with transaction.atomic():
self._DEDUCTION_OBJ.intervention.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED) self.deduction.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.account.mark_as_edited(self.user, edit_comment=DEDUCTION_REMOVED)
self._DEDUCTION_OBJ.delete() self.deduction.delete()
def check_for_recorded_instance(self): def check_for_recorded_instance(self):
if self._DEDUCTION_OBJ.intervention.is_recorded: if self.deduction.intervention.is_recorded:
self.block_form() self.block_form()

View File

@@ -6,11 +6,11 @@ Created on: 18.08.22
""" """
from intervention.models import InterventionDocument from intervention.models import InterventionDocument
from konova.forms.modals import NewDocumentModalForm, EditDocumentModalForm, RemoveDocumentModalForm from konova.forms.modals import NewDocumentModalForm
class NewInterventionDocumentModalForm(NewDocumentModalForm): class NewInterventionDocumentModalForm(NewDocumentModalForm):
_DOCUMENT_CLS = InterventionDocument document_model = InterventionDocument
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" Extension of regular NewDocumentModalForm """ Extension of regular NewDocumentModalForm
@@ -28,31 +28,3 @@ class NewInterventionDocumentModalForm(NewDocumentModalForm):
self.instance.send_data_to_egon() self.instance.send_data_to_egon()
return doc return doc
class EditInterventionDocumentModalForm(EditDocumentModalForm):
_DOCUMENT_CLS = InterventionDocument
def save(self, *args, **kwargs):
""" Extension of regular EditDocumentModalForm
Checks whether payments exist on the intervention and sends the data to EGON
Args:
*args ():
**kwargs ():
Returns:
"""
doc = super().save(*args, **kwargs)
self.instance.send_data_to_egon()
return doc
class RemoveInterventionDocumentModalForm(RemoveDocumentModalForm):
_DOCUMENT_CLS = InterventionDocument
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.instance.send_data_to_egon()

View File

@@ -1,22 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 08.11.25
"""
from django.shortcuts import get_object_or_404
from compensation.models import Compensation
from konova.forms.modals import RemoveModalForm
class RemoveCompensationFromInterventionModalForm(RemoveModalForm):
""" Specific form for removing a compensation from an intervention
"""
def __init__(self, *args, **kwargs):
# The 'instance' that is pushed into the constructor by the generic base class points to the
# intervention instead of the compensation, which shall be deleted. Therefore we need to fetch the compensation
# and replace the instance parameter with that object
instance = get_object_or_404(Compensation, id=kwargs.pop("comp_id"))
kwargs["instance"] = instance
super().__init__(*args, **kwargs)

View File

@@ -1,11 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 21.10.25
"""
from intervention.models import Intervention
from konova.forms.modals import ResubmissionModalForm
class InterventionResubmissionModalForm(ResubmissionModalForm):
_MODEL_CLS = Intervention

View File

@@ -7,10 +7,9 @@ Created on: 18.08.22
""" """
from django import forms from django import forms
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from intervention.models import RevocationDocument, Revocation from intervention.models import RevocationDocument
from konova.forms.modals import BaseModalForm, RemoveModalForm from konova.forms.modals import BaseModalForm, RemoveModalForm
from konova.utils import validators from konova.utils import validators
from konova.utils.message_templates import REVOCATION_ADDED, REVOCATION_EDITED from konova.utils.message_templates import REVOCATION_ADDED, REVOCATION_EDITED
@@ -76,8 +75,7 @@ class EditRevocationModalForm(NewRevocationModalForm):
revocation = None revocation = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
revocation_id = kwargs.pop("revocation_id", None) self.revocation = kwargs.pop("revocation", None)
self.revocation = get_object_or_404(Revocation, id=revocation_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit revocation") self.form_title = _("Edit revocation")
try: try:
@@ -106,8 +104,8 @@ class RemoveRevocationModalForm(RemoveModalForm):
revocation = None revocation = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
revocation_id = kwargs.pop("revocation_id", None) revocation = kwargs.pop("revocation", None)
self.revocation = get_object_or_404(Revocation, id=revocation_id) self.revocation = revocation
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def save(self): def save(self):

View File

@@ -280,7 +280,7 @@ class EditRevocationModalFormTestCase(NewRevocationModalFormTestCase):
data, data,
request=self.request, request=self.request,
instance=self.intervention, instance=self.intervention,
revocation_id=self.revoc.id revocation=self.revoc
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)
obj = form.save() obj = form.save()
@@ -302,7 +302,7 @@ class RemoveRevocationModalFormTestCase(EditRevocationModalFormTestCase):
form = RemoveRevocationModalForm( form = RemoveRevocationModalForm(
request=self.request, request=self.request,
instance=self.intervention, instance=self.intervention,
revocation_id=self.revoc.id, revocation=self.revoc,
) )
self.assertEqual(form.instance, self.intervention) self.assertEqual(form.instance, self.intervention)
self.assertEqual(form.revocation, self.revoc) self.assertEqual(form.revocation, self.revoc)
@@ -317,7 +317,7 @@ class RemoveRevocationModalFormTestCase(EditRevocationModalFormTestCase):
data, data,
request=self.request, request=self.request,
instance=self.intervention, instance=self.intervention,
revocation_id=self.revoc.id revocation=self.revoc
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)
form.save() form.save()

View File

@@ -8,40 +8,39 @@ Created on: 30.11.20
from django.urls import path from django.urls import path
from intervention.autocomplete.intervention import InterventionAutocomplete from intervention.autocomplete.intervention import InterventionAutocomplete
from intervention.views.check import InterventionCheckView from intervention.views.check import check_view
from intervention.views.compensation import RemoveCompensationFromInterventionView from intervention.views.compensation import remove_compensation_view
from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \ from intervention.views.deduction import NewInterventionDeductionView, EditInterventionDeductionView, \
RemoveInterventionDeductionView RemoveInterventionDeductionView
from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \ from intervention.views.document import NewInterventionDocumentView, GetInterventionDocumentView, \
RemoveInterventionDocumentView, EditInterventionDocumentView RemoveInterventionDocumentView, EditInterventionDocumentView
from intervention.views.intervention import InterventionIndexView, InterventionIdentifierGeneratorView, \ from intervention.views.intervention import index_view, new_view, new_id_view, detail_view, edit_view, remove_view
InterventionDetailView, NewInterventionFormView, EditInterventionFormView, RemoveInterventionView
from intervention.views.log import InterventionLogView from intervention.views.log import InterventionLogView
from intervention.views.record import InterventionRecordView from intervention.views.record import InterventionRecordView
from intervention.views.report import InterventionReportView from intervention.views.report import report_view
from intervention.views.resubmission import InterventionResubmissionView from intervention.views.resubmission import InterventionResubmissionView
from intervention.views.revocation import NewRevocationView, GetRevocationDocumentView, EditRevocationView, \ from intervention.views.revocation import new_revocation_view, edit_revocation_view, remove_revocation_view, \
RemoveRevocationView get_revocation_view
from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView from intervention.views.share import InterventionShareFormView, InterventionShareByTokenView
app_name = "intervention" app_name = "intervention"
urlpatterns = [ urlpatterns = [
path("", InterventionIndexView.as_view(), name="index"), path("", index_view, name="index"),
path('new/', NewInterventionFormView.as_view(), name='new'), path('new/', new_view, name='new'),
path('new/id', InterventionIdentifierGeneratorView.as_view(), name='new-id'), path('new/id', new_id_view, name='new-id'),
path('<id>', InterventionDetailView.as_view(), name='detail'), path('<id>', detail_view, name='detail'),
path('<id>/log', InterventionLogView.as_view(), name='log'), path('<id>/log', InterventionLogView.as_view(), name='log'),
path('<id>/edit', EditInterventionFormView.as_view(), name='edit'), path('<id>/edit', edit_view, name='edit'),
path('<id>/remove', RemoveInterventionView.as_view(), name='remove'), path('<id>/remove', remove_view, name='remove'),
path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'), path('<id>/share/<token>', InterventionShareByTokenView.as_view(), name='share-token'),
path('<id>/share', InterventionShareFormView.as_view(), name='share-form'), path('<id>/share', InterventionShareFormView.as_view(), name='share-form'),
path('<id>/check', InterventionCheckView.as_view(), name='check'), path('<id>/check', check_view, name='check'),
path('<id>/record', InterventionRecordView.as_view(), name='record'), path('<id>/record', InterventionRecordView.as_view(), name='record'),
path('<id>/report', InterventionReportView.as_view(), name='report'), path('<id>/report', report_view, name='report'),
path('<id>/resub', InterventionResubmissionView.as_view(), name='resubmission-create'), path('<id>/resub', InterventionResubmissionView.as_view(), name='resubmission-create'),
# Compensations # Compensations
path('<id>/compensation/<comp_id>/remove', RemoveCompensationFromInterventionView.as_view(), name='remove-compensation'), path('<id>/compensation/<comp_id>/remove', remove_compensation_view, name='remove-compensation'),
# Documents # Documents
path('<id>/document/new/', NewInterventionDocumentView.as_view(), name='new-doc'), path('<id>/document/new/', NewInterventionDocumentView.as_view(), name='new-doc'),
@@ -55,10 +54,10 @@ urlpatterns = [
path('<id>/deduction/<deduction_id>/remove', RemoveInterventionDeductionView.as_view(), name='remove-deduction'), path('<id>/deduction/<deduction_id>/remove', RemoveInterventionDeductionView.as_view(), name='remove-deduction'),
# Revocation routes # Revocation routes
path('<id>/revocation/new', NewRevocationView.as_view(), name='new-revocation'), path('<id>/revocation/new', new_revocation_view, name='new-revocation'),
path('<id>/revocation/<revocation_id>/edit', EditRevocationView.as_view(), name='edit-revocation'), path('<id>/revocation/<revocation_id>/edit', edit_revocation_view, name='edit-revocation'),
path('<id>/revocation/<revocation_id>/remove', RemoveRevocationView.as_view(), name='remove-revocation'), path('<id>/revocation/<revocation_id>/remove', remove_revocation_view, name='remove-revocation'),
path('revocation/<doc_id>', GetRevocationDocumentView.as_view(), name='get-doc-revocation'), path('revocation/<doc_id>', get_revocation_view, name='get-doc-revocation'),
# Autocomplete # Autocomplete
path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="autocomplete"), path("atcmplt/interventions", InterventionAutocomplete.as_view(), name="autocomplete"),

View File

@@ -5,24 +5,35 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.http import HttpRequest
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from intervention.forms.modals.check import CheckModalForm from intervention.forms.modals.check import CheckModalForm
from intervention.models import Intervention from intervention.models import Intervention
from konova.views.modal import AbstractModalFormView from konova.decorators import registration_office_group_required, shared_access_required
from konova.utils.message_templates import INTERVENTION_INVALID
class InterventionCheckView(LoginRequiredMixin, AbstractModalFormView): @login_required
_MODEL_CLS = Intervention @registration_office_group_required
_FORM_CLS = CheckModalForm @shared_access_required(Intervention, "id")
_MSG_SUCCESS = _("Check performed") def check_view(request: HttpRequest, id: str):
_REDIRECT_URL = "intervention:detail" """ Renders check form for an intervention
def _user_has_permission(self, user, **kwargs): Args:
return user.is_zb_user() request (HttpRequest): The incoming request
id (str): Intervention's id
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
)
def _get_redirect_url(self, *args, **kwargs):
redirect_url = super()._get_redirect_url(*args, **kwargs)
redirect_url += "#related_data"
return redirect_url

View File

@@ -5,36 +5,42 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.core.exceptions import ObjectDoesNotExist
from django.http import HttpRequest, Http404
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse from django.urls import reverse
from compensation.models import Compensation
from intervention.forms.modals.remove import RemoveCompensationFromInterventionModalForm
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import shared_access_required, login_required_modal
from konova.forms.modals import RemoveModalForm
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from konova.views.remove import BaseRemoveModalFormView
class RemoveCompensationFromInterventionView(LoginRequiredMixin, BaseRemoveModalFormView): @login_required_modal
_MODEL_CLS = Intervention @login_required
_FORM_CLS = RemoveCompensationFromInterventionModalForm @shared_access_required(Intervention, "id")
_MSG_SUCCESS = COMPENSATION_REMOVED_TEMPLATE def remove_compensation_view(request: HttpRequest, id: str, comp_id: str):
_REDIRECT_URL = "intervention:detail" """ Renders a modal view for removing the compensation
def _user_has_shared_access(self, user, **kwargs): Args:
compensation_id = kwargs.get("comp_id", None) request (HttpRequest): The incoming request
compensation = get_object_or_404(Compensation, id=compensation_id) id (str): The compensation's id
return compensation.is_shared_with(user)
def _user_has_permission(self, user, **kwargs): Returns:
return user.is_default_user()
def _get_msg_success(self, *args, **kwargs): """
compensation_id = kwargs.get("comp_id", None) intervention = get_object_or_404(Intervention, id=id)
compensation = get_object_or_404(Compensation, id=compensation_id) try:
return self._MSG_SUCCESS.format(compensation.identifier) 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",
)
def _get_redirect_url(self, *args, **kwargs):
obj = kwargs.get("obj")
return reverse(self._REDIRECT_URL, args=(obj.id,)) + "#related_data"

View File

@@ -5,27 +5,51 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from intervention.models import Intervention 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 from konova.views.deduction import AbstractNewDeductionView, AbstractEditDeductionView, AbstractRemoveDeductionView
_INTERVENTION_DETAIL_URL_NAME = "intervention:detail"
class NewInterventionDeductionView(LoginRequiredMixin, AbstractNewDeductionView): class NewInterventionDeductionView(AbstractNewDeductionView):
_MODEL_CLS = Intervention def _custom_check(self, obj):
_MSG_SUCCESS = DEDUCTION_ADDED pass
_REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME
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): class EditInterventionDeductionView(AbstractEditDeductionView):
_MODEL_CLS = Intervention def _custom_check(self, obj):
_MSG_SUCCESS = DEDUCTION_EDITED pass
_REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME
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): class RemoveInterventionDeductionView(AbstractRemoveDeductionView):
_MODEL_CLS = Intervention def _custom_check(self, obj):
_MSG_SUCCESS = DEDUCTION_REMOVED pass
_REDIRECT_URL = _INTERVENTION_DETAIL_URL_NAME
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)

View File

@@ -5,33 +5,59 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from intervention.forms.modals.document import NewInterventionDocumentModalForm, EditInterventionDocumentModalForm, \ from django.contrib.auth.decorators import login_required
RemoveInterventionDocumentModalForm from django.utils.decorators import method_decorator
from intervention.forms.modals.document import NewInterventionDocumentModalForm
from intervention.models import Intervention, InterventionDocument from intervention.models import Intervention, InterventionDocument
from konova.decorators import default_group_required, shared_access_required
from konova.forms.modals import EditDocumentModalForm
from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \ from konova.views.document import AbstractNewDocumentView, AbstractGetDocumentView, AbstractRemoveDocumentView, \
AbstractEditDocumentView AbstractEditDocumentView
class NewInterventionDocumentView(AbstractNewDocumentView): class NewInterventionDocumentView(AbstractNewDocumentView):
_MODEL_CLS = Intervention model = Intervention
_DOCUMENT_MODEL = InterventionDocument form = NewInterventionDocumentModalForm
_FORM_CLS = NewInterventionDocumentModalForm redirect_url = "intervention:detail"
_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 GetInterventionDocumentView(AbstractGetDocumentView): class GetInterventionDocumentView(AbstractGetDocumentView):
_MODEL_CLS = Intervention model = Intervention
_DOCUMENT_CLS = InterventionDocument document_model = InterventionDocument
@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 RemoveInterventionDocumentView(AbstractRemoveDocumentView): class RemoveInterventionDocumentView(AbstractRemoveDocumentView):
_MODEL_CLS = Intervention model = Intervention
_DOCUMENT_CLS = InterventionDocument document_model = InterventionDocument
_FORM_CLS = RemoveInterventionDocumentModalForm
_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 EditInterventionDocumentView(AbstractEditDocumentView): class EditInterventionDocumentView(AbstractEditDocumentView):
_MODEL_CLS = Intervention model = Intervention
_DOCUMENT_CLS = InterventionDocument document_model = InterventionDocument
_FORM_CLS = EditInterventionDocumentModalForm form = EditDocumentModalForm
_REDIRECT_URL = "intervention:detail" 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)

View File

@@ -5,111 +5,293 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib import messages
from django.shortcuts import get_object_or_404 from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, HttpRequest
from django.shortcuts import get_object_or_404, render, redirect
from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from intervention.forms.intervention import EditInterventionForm, NewInterventionForm from intervention.forms.intervention import EditInterventionForm, NewInterventionForm
from intervention.models import Intervention from intervention.models import Intervention
from intervention.tables import InterventionTable from intervention.tables import InterventionTable
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE from konova.contexts import BaseContext
from konova.views.identifier import AbstractIdentifierGeneratorView from konova.decorators import default_group_required, shared_access_required, any_group_check, login_required_modal, \
from konova.views.form import AbstractNewGeometryFormView, AbstractEditGeometryFormView uuid_required
from konova.views.index import AbstractIndexView from konova.forms import SimpleGeomForm
from konova.views.detail import BaseDetailView from konova.forms.modals import RemoveModalForm
from konova.views.remove import BaseRemoveModalFormView 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, RECORDED_BLOCKS_EDIT, \
CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \
GEOMETRIES_IGNORED_TEMPLATE
class InterventionIndexView(LoginRequiredMixin, AbstractIndexView): @login_required
_INDEX_TABLE_CLS = InterventionTable @any_group_check
_TAB_TITLE = _("Interventions - Overview") def index_view(request: HttpRequest):
"""
Renders the index view for Interventions
def _get_queryset(self): Args:
qs = Intervention.objects.filter( request (HttpRequest): The incoming request
deleted=None,
).select_related( Returns:
"legal" A rendered view
).order_by( """
"-modified__timestamp" template = "generic_index.html"
)
return qs # Filtering by user access is performed in table filter inside InterventionTableFilter class
interventions = Intervention.objects.filter(
deleted=None, # not deleted
).select_related(
"legal"
).order_by(
"-modified__timestamp"
)
table = InterventionTable(
request=request,
queryset=interventions
)
context = {
"table": table,
TAB_TITLE_IDENTIFIER: _("Interventions - Overview"),
}
context = BaseContext(request, context).context
return render(request, template, context)
class NewInterventionFormView(AbstractNewGeometryFormView): @login_required
_MODEL_CLS = Intervention @default_group_required
_FORM_CLS = NewInterventionForm def new_view(request: HttpRequest):
_TEMPLATE = "intervention/form/view.html" """
_REDIRECT_URL = "intervention:detail" Renders a view for a new intervention creation
_TAB_TITLE = _("New intervention")
Args:
request (HttpRequest): The incoming request
Returns:
"""
template = "intervention/form/view.html"
data_form = NewInterventionForm(request.POST or None)
geom_form = SimpleGeomForm(request.POST or None, read_only=False)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
generated_identifier = data_form.cleaned_data.get("identifier", None)
intervention = data_form.save(request.user, geom_form)
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",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("New intervention"),
}
context = BaseContext(request, context).context
return render(request, template, context)
class EditInterventionFormView(AbstractEditGeometryFormView): @login_required
_MODEL_CLS = Intervention @default_group_required
_FORM_CLS = EditInterventionForm def new_id_view(request: HttpRequest):
_TEMPLATE = "intervention/form/view.html" """ JSON endpoint
_REDIRECT_URL = "intervention:detail"
_TAB_TITLE = _("Edit {}")
Provides fetching of free identifiers for e.g. AJAX calls
class InterventionIdentifierGeneratorView(LoginRequiredMixin, AbstractIdentifierGeneratorView): """
_MODEL_CLS = Intervention tmp_intervention = Intervention()
_REDIRECT_URL = "intervention:index" identifier = tmp_intervention.generate_new_identifier()
while Intervention.objects.filter(identifier=identifier).exists():
identifier = tmp_intervention.generate_new_identifier()
class InterventionDetailView(BaseDetailView): return JsonResponse(
_MODEL_CLS = Intervention data={
_TEMPLATE = "intervention/detail/view.html" "gen_data": identifier
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
)
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,
} }
return context )
class RemoveInterventionView(LoginRequiredMixin, BaseRemoveModalFormView):
_MODEL_CLS = Intervention @login_required
_REDIRECT_URL = "intervention:index" @any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for viewing an intervention's data
Args:
request (HttpRequest): The incoming request
id (str): The intervention's id
Returns:
"""
template = "intervention/detail/view.html"
# Fetch data, filter out deleted related data
intervention = get_object_or_404(
Intervention.objects.select_related(
"geometry",
"legal",
"responsible",
).prefetch_related(
"legal__revocations",
),
id=id,
deleted=None
)
compensations = intervention.compensations.filter(
deleted=None,
)
_user = request.user
is_data_shared = intervention.is_shared_with(user=_user)
geom_form = SimpleGeomForm(
instance=intervention,
)
last_checked = intervention.get_last_checked_action()
last_checked_tooltip = ""
if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(
last_checked.get_timestamp_str_formatted(),
last_checked.user
)
has_payment_without_document = intervention.payments.exists() and not intervention.get_documents()[1].exists()
requesting_user_is_only_shared_user = intervention.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = {
"obj": intervention,
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"compensations": compensations,
"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, 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
Args:
request (HttpRequest): The incoming request
Returns:
"""
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)
# Create forms, initialize with values from db/from POST request
data_form = EditInterventionForm(request.POST or None, instance=intervention)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=intervention)
if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid():
# The data form takes the geom form for processing, as well as the performing user
# Save the current state of recorded|checked to inform the user in case of a status reset due to editing
intervention_is_checked = intervention.checked is not None
intervention = data_form.save(request.user, geom_form)
messages.success(request, _("Intervention {} edited").format(intervention.identifier))
if intervention_is_checked:
messages.info(request, CHECK_STATE_RESET)
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",)
else:
# For clarification: nothing in this case
pass
context = {
"form": data_form,
"geom_form": geom_form,
TAB_TITLE_IDENTIFIER: _("Edit {}").format(intervention.identifier),
}
context = BaseContext(request, context).context
return render(request, template, context)
@login_required_modal
@login_required
@default_group_required
@shared_access_required(Intervention, "id")
def remove_view(request: HttpRequest, id: str):
""" Renders a remove view for this intervention
Args:
request (HttpRequest): The incoming request
id (str): The uuid id as string
Returns:
"""
obj = Intervention.objects.get(id=id)
identifier = obj.identifier
form = RemoveModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
_("{} removed").format(identifier),
redirect_url=reverse("intervention:index")
)

View File

@@ -5,11 +5,19 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import shared_access_required, default_group_required
from konova.views.log import AbstractLogView from konova.views.log import AbstractLogView
class InterventionLogView(LoginRequiredMixin, AbstractLogView): class InterventionLogView(AbstractLogView):
_MODEL_CLS = Intervention model = Intervention
@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)

View File

@@ -5,12 +5,19 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import conservation_office_group_required, shared_access_required
from konova.views.record import AbstractRecordView from konova.views.record import AbstractRecordView
class InterventionRecordView(LoginRequiredMixin, AbstractRecordView): class InterventionRecordView(AbstractRecordView):
_MODEL_CLS = Intervention model = Intervention
_REDIRECT_URL = "intervention:detail"
@method_decorator(login_required)
@method_decorator(conservation_office_group_required)
@method_decorator(shared_access_required(Intervention, "id"))
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)

View File

@@ -5,41 +5,72 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from intervention.models import Intervention from intervention.models import Intervention
from konova.sub_settings.django_settings import BASE_URL from konova.contexts import BaseContext
from konova.utils.qrcode import QrCode from konova.decorators import uuid_required
from konova.views.report import AbstractReportView from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
class InterventionReportView(AbstractReportView): @uuid_required
_TEMPLATE = 'intervention/report/report.html' def report_view(request: HttpRequest, id: str):
_MODEL = Intervention """ Renders the public report view
def _get_report_context(self, obj: Intervention): Args:
""" Returns the specific context needed for an intervention report request (HttpRequest): The incoming request
id (str): The id of the intervention
Args: Returns:
obj (Intervention): The object for the report
Returns: """
dict: The object specific context for rendering the report template = "intervention/report/report.html"
""" intervention = get_object_or_404(Intervention, id=id)
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 { tab_title = _("Report {}").format(intervention.identifier)
"deductions": distinct_deductions, # If intervention is not recorded (yet or currently) we need to render another template without any data
"qrcode": { if not intervention.is_ready_for_publish():
"img": qrcode_report.get_img(), template = "report/unavailable.html"
"url": qrcode_report.get_content(), context = {
}, TAB_TITLE_IDENTIFIER: tab_title,
"qrcode_lanis": {
"img": qrcode_lanis.get_img(),
"url": qrcode_lanis.get_content(),
},
"tables_scrollable": False,
} }
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_url = request.build_absolute_uri(reverse("intervention:report", args=(id,)))
qrcode_img = generate_qr_code(qrcode_url, 10)
qrcode_lanis_url = intervention.get_LANIS_link()
qrcode_img_lanis = generate_qr_code(qrcode_lanis_url, 7)
context = {
"obj": intervention,
"deductions": distinct_deductions,
"qrcode": {
"img": qrcode_img,
"url": qrcode_url,
},
"qrcode_lanis": {
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"geom_form": geom_form,
"parcels": parcels,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title,
}
context = BaseContext(request, context).context
return render(request, template, context)

View File

@@ -5,12 +5,22 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from intervention.forms.modals.resubmission import InterventionResubmissionModalForm from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import default_group_required, shared_access_required, login_required_modal
from konova.views.resubmission import AbstractResubmissionView from konova.views.resubmission import AbstractResubmissionView
class InterventionResubmissionView(AbstractResubmissionView): class InterventionResubmissionView(AbstractResubmissionView):
_MODEL_CLS = Intervention model = Intervention
_FORM_CLS = InterventionResubmissionModalForm redirect_url_base = "intervention:detail"
_REDIRECT_URL = "intervention:detail" form_action_url_base = "intervention:resubmission-create"
@method_decorator(login_required_modal)
@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)

View File

@@ -6,71 +6,113 @@ Created on: 19.08.22
""" """
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin from django.contrib.auth.decorators import login_required
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import get_object_or_404, redirect from django.shortcuts import get_object_or_404, redirect
from django.urls import reverse
from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \ from intervention.forms.modals.revocation import NewRevocationModalForm, EditRevocationModalForm, \
RemoveRevocationModalForm RemoveRevocationModalForm
from intervention.models import Intervention, RevocationDocument from intervention.models import Intervention, RevocationDocument, Revocation
from konova.decorators import default_group_required, shared_access_required, login_required_modal
from konova.utils.documents import get_document from konova.utils.documents import get_document
from konova.utils.message_templates import DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED, REVOCATION_ADDED from konova.utils.message_templates import REVOCATION_ADDED, DATA_UNSHARED, REVOCATION_EDITED, REVOCATION_REMOVED
from konova.views.modal import AbstractModalFormView, AbstractBaseView
class BaseRevocationView(LoginRequiredMixin, AbstractModalFormView): @login_required
_MODEL_CLS = Intervention @default_group_required
_REDIRECT_URL = "intervention:detail" @shared_access_required(Intervention, "id")
def new_revocation_view(request: HttpRequest, id: str):
""" Renders sharing form for an intervention
class Meta: Args:
abstract = True request (HttpRequest): The incoming request
id (str): Intervention's id
def _user_has_permission(self, user, **kwargs): Returns:
return user.is_default_user()
def _get_redirect_url(self, *args, **kwargs): """
url = super()._get_redirect_url(*args, **kwargs) intervention = get_object_or_404(Intervention, id=id)
return f"{url}#related_data" 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"
)
class NewRevocationView(BaseRevocationView): @login_required
_FORM_CLS = NewRevocationModalForm @default_group_required
_MSG_SUCCESS = REVOCATION_ADDED def get_revocation_view(request: HttpRequest, doc_id: str):
""" Returns the revocation document as downloadable file
Wraps the generic document fetcher function from konova.utils.
Args:
request (HttpRequest): The incoming request
doc_id (str): The document id
Returns:
"""
doc = get_object_or_404(RevocationDocument, id=doc_id)
# File download only possible if related instance is shared with user
if not doc.instance.legal.intervention.users.filter(id=request.user.id):
messages.info(
request,
DATA_UNSHARED
)
return redirect("intervention:detail", id=doc.instance.id)
return get_document(doc)
class EditRevocationView(BaseRevocationView): @login_required
_FORM_CLS = EditRevocationModalForm @default_group_required
_MSG_SUCCESS = REVOCATION_EDITED @shared_access_required(Intervention, "id")
def edit_revocation_view(request: HttpRequest, id: str, revocation_id: str):
""" 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
Returns:
"""
intervention = get_object_or_404(Intervention, id=id)
revocation = get_object_or_404(Revocation, id=revocation_id)
form = EditRevocationModalForm(request.POST or None, request.FILES or None, instance=intervention, revocation=revocation, request=request)
return form.process_request(
request,
REVOCATION_EDITED,
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
)
class RemoveRevocationView(BaseRevocationView): @login_required_modal
_FORM_CLS = RemoveRevocationModalForm @login_required
_MSG_SUCCESS = REVOCATION_REMOVED @default_group_required
@shared_access_required(Intervention, "id")
def remove_revocation_view(request: HttpRequest, id: str, revocation_id: str):
""" Renders a remove view for a revocation
Args:
request (HttpRequest): The incoming request
id (str): The intervention's id as string
revocation_id (str): The revocation's id as string
class GetRevocationDocumentView(LoginRequiredMixin, AbstractBaseView): Returns:
_MODEL_CLS = RevocationDocument
_REDIRECT_URL = "intervention:detail"
def get(self, request: HttpRequest, doc_id: str): """
doc = get_object_or_404(RevocationDocument, id=doc_id) intervention = get_object_or_404(Intervention, id=id)
# File download only possible if related instance is shared with user revocation = get_object_or_404(Revocation, id=revocation_id)
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)
def _user_has_permission(self, user, **kwargs): form = RemoveRevocationModalForm(request.POST or None, instance=intervention, revocation=revocation, request=request)
return user.is_default_user() return form.process_request(
request,
REVOCATION_REMOVED,
redirect_url=reverse("intervention:detail", args=(intervention.id,)) + "#related_data"
)
def _user_has_shared_access(self, user, **kwargs):
obj = get_object_or_404(self._MODEL_CLS, id=kwargs.get("doc_id"))
assert obj is not None
return obj.instance.intervention.is_shared_with(user)
def _get_redirect_url(self, *args, **kwargs):
url = super()._get_redirect_url(*args, **kwargs)
return f"{url}#related_data"

View File

@@ -5,15 +5,29 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22 Created on: 19.08.22
""" """
from django.contrib.auth.decorators import login_required
from django.utils.decorators import method_decorator
from intervention.models import Intervention from intervention.models import Intervention
from konova.decorators import default_group_required, shared_access_required, login_required_modal
from konova.views.share import AbstractShareByTokenView, AbstractShareFormView from konova.views.share import AbstractShareByTokenView, AbstractShareFormView
class InterventionShareByTokenView(AbstractShareByTokenView): class InterventionShareByTokenView(AbstractShareByTokenView):
_MODEL_CLS = Intervention model = Intervention
_REDIRECT_URL = "intervention:detail" redirect_url = "intervention:detail"
@method_decorator(login_required)
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
class InterventionShareFormView(AbstractShareFormView): class InterventionShareFormView(AbstractShareFormView):
_MODEL_CLS = Intervention model = Intervention
_REDIRECT_URL = "intervention:detail"
@method_decorator(login_required_modal)
@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)

View File

@@ -10,18 +10,21 @@ from abc import abstractmethod
from django import forms from django import forms
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount
from konova.models import BaseObject
class BaseForm(forms.Form): class BaseForm(forms.Form):
""" """
Basic form for that holds attributes needed in all other forms Basic form for that holds attributes needed in all other forms
""" """
template = None
action_url = None action_url = None
action_btn_label = _("Save") action_btn_label = _("Save")
form_title = None form_title = None
cancel_redirect = None cancel_redirect = None
form_caption = None form_caption = None
instance = None # The data holding model object instance = None # The data holding model object
user = None # The performing user
request = None request = None
form_attrs = {} # Holds additional attributes, that can be used in the template form_attrs = {} # Holds additional attributes, that can be used in the template
has_required_fields = False # Automatically set. Triggers hint rendering in templates has_required_fields = False # Automatically set. Triggers hint rendering in templates
@@ -30,7 +33,6 @@ class BaseForm(forms.Form):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.instance = kwargs.pop("instance", None) self.instance = kwargs.pop("instance", None)
self.user = kwargs.pop("user", None)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
if self.request is not None: if self.request is not None:
self.user = self.request.user self.user = self.request.user
@@ -40,10 +42,11 @@ class BaseForm(forms.Form):
self.has_required_fields = True self.has_required_fields = True
break break
self.check_for_recorded_instance()
self.__check_valid_label_input_ratio() self.__check_valid_label_input_ratio()
@abstractmethod @abstractmethod
def save(self, *arg, **kwargs): def save(self):
# To be implemented in subclasses! # To be implemented in subclasses!
pass pass
@@ -133,3 +136,34 @@ class BaseForm(forms.Form):
set_class = self.fields[field].widget.attrs.get("class", "") set_class = self.fields[field].widget.attrs.get("class", "")
set_class = set_class.replace(cls, "") set_class = set_class.replace(cls, "")
self.fields[field].widget.attrs["class"] = set_class self.fields[field].widget.attrs["class"] = set_class
def check_for_recorded_instance(self):
""" Checks if the instance is recorded and runs some special logic if yes
If the instance is recorded, the form shall not display any possibility to
edit any data. Instead, the users should get some information about why they can not edit anything.
There are situations where the form should be rendered regularly,
e.g deduction forms for (recorded) eco accounts.
Returns:
"""
is_none = self.instance is None
is_other_data_type = not isinstance(self.instance, BaseObject)
if is_none or is_other_data_type:
# Do nothing
return
if self.instance.is_recorded:
self.block_form()
def block_form(self):
"""
Overwrites template, providing no actions
Returns:
"""
self.template = "form/recorded_no_edit.html"

View File

@@ -35,7 +35,6 @@ class SimpleGeomForm(BaseForm):
disabled=False, disabled=False,
) )
_num_geometries_ignored: int = 0 _num_geometries_ignored: int = 0
empty = False
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
self.read_only = kwargs.pop("read_only", True) self.read_only = kwargs.pop("read_only", True)
@@ -50,11 +49,11 @@ class SimpleGeomForm(BaseForm):
raise AttributeError raise AttributeError
geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP) geojson = self.instance.geometry.as_feature_collection(srid=DEFAULT_SRID_RLP)
geojson = self._set_geojson_properties(geojson, title=self.instance.identifier or None) self._set_geojson_properties(geojson, title=self.instance.identifier or None)
geom = json.dumps(geojson) geom = json.dumps(geojson)
except AttributeError: except AttributeError:
# If no geometry exists for this form, we simply set the value to None and zoom to the maximum level # If no geometry exists for this form, we simply set the value to None and zoom to the maximum level
geom = json.dumps({}) geom = ""
self.empty = True self.empty = True
self.initialize_form_field("output", geom) self.initialize_form_field("output", geom)
@@ -63,18 +62,18 @@ class SimpleGeomForm(BaseForm):
super().is_valid() super().is_valid()
is_valid = True is_valid = True
# Make sure invalid geometry is properly rendered again to the user # Get geojson from form
# Therefore: write submitted data back into form field geom = self.data.get("output", None)
# (does not matter whether we know if it is valid or invalid) if geom is None or len(geom) == 0:
submitted_data = self.data["output"] # empty geometry is a valid geometry
submitted_data = json.loads(submitted_data) self.cleaned_data["output"] = MultiPolygon(srid=DEFAULT_SRID_RLP).ewkt
submitted_data = self._set_geojson_properties(submitted_data) return is_valid
self.initialize_form_field("output", json.dumps(submitted_data))
# Get geojson from form for validity checking
geom = self.data.get("output", json.dumps({}))
geom = json.loads(geom) geom = json.loads(geom)
# 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"])
# Initialize features list with empty MultiPolygon, so that an empty input will result in a # Initialize features list with empty MultiPolygon, so that an empty input will result in a
# proper empty MultiPolygon object # proper empty MultiPolygon object
features = [] features = []
@@ -85,23 +84,20 @@ class SimpleGeomForm(BaseForm):
"MultiPolygon", "MultiPolygon",
"MultiPolygon25D", "MultiPolygon25D",
] ]
# Check validity for each feature of the geometry
for feature in features_json: for feature in features_json:
feature_geom = feature.get("geometry", feature) feature_geom = feature.get("geometry", feature)
if feature_geom is None: if feature_geom is None:
# Fallback for rare cases where a feature does not contain any geometry # Fallback for rare cases where a feature does not contain any geometry
continue continue
# Try to create a geometry object from the single feature
feature_geom = json.dumps(feature_geom) feature_geom = json.dumps(feature_geom)
g = gdal.OGRGeometry(feature_geom, srs=DEFAULT_SRID_RLP) g = gdal.OGRGeometry(feature_geom, srs=DEFAULT_SRID_RLP)
geometry_has_unwanted_dimensions = g.coord_dim > 2 flatten_geometry = g.coord_dim > 2
if geometry_has_unwanted_dimensions: if flatten_geometry:
g = self.__flatten_geom_to_2D(g) g = self.__flatten_geom_to_2D(g)
geometry_type_is_accepted = g.geom_type not in accepted_ogr_types if g.geom_type not in accepted_ogr_types:
if geometry_type_is_accepted:
self.add_error("output", _("Only surfaces allowed. Points or lines must be buffered.")) self.add_error("output", _("Only surfaces allowed. Points or lines must be buffered."))
is_valid &= False is_valid &= False
return is_valid return is_valid
@@ -113,33 +109,27 @@ class SimpleGeomForm(BaseForm):
self._num_geometries_ignored += 1 self._num_geometries_ignored += 1
continue 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) g = Polygon.from_ewkt(g.ewkt)
is_valid &= g.valid is_valid &= g.valid
if not g.valid: if not g.valid:
self.add_error("output", g.valid_reason) self.add_error("output", g.valid_reason)
return is_valid return is_valid
# If the resulting polygon is just a single polygon, we add it to the list of properly casted features
if isinstance(g, Polygon): if isinstance(g, Polygon):
features.append(g) features.append(g)
elif isinstance(g, MultiPolygon): elif isinstance(g, MultiPolygon):
# The resulting polygon could be of type MultiPolygon (due to multiple surfaces)
# If so, we extract all polygons from the MultiPolygon and extend the casted features list
features.extend(list(g)) features.extend(list(g))
# Unionize all polygon features into one new MultiPolygon # Unionize all geometry features into one new MultiPolygon
if features: if features:
form_geom = MultiPolygon(*features, srid=DEFAULT_SRID_RLP).unary_union form_geom = MultiPolygon(*features, srid=DEFAULT_SRID_RLP).unary_union
else: 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) form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided. # Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
form_geom = Geometry.cast_to_multipolygon(form_geom) form_geom = Geometry.cast_to_multipolygon(form_geom)
# Write unionized Multipolygon back into cleaned data # Write unioned Multipolygon into cleaned data
if self.cleaned_data is None: if self.cleaned_data is None:
self.cleaned_data = {} self.cleaned_data = {}
self.cleaned_data["output"] = form_geom.ewkt self.cleaned_data["output"] = form_geom.ewkt
@@ -262,8 +252,6 @@ class SimpleGeomForm(BaseForm):
""" """
features = geojson.get("features", []) features = geojson.get("features", [])
for feature in features: for feature in features:
if not feature.get("properties", None):
feature["properties"] = {}
feature["properties"]["editable"] = not self.read_only feature["properties"]["editable"] = not self.read_only
if title: if title:
feature["properties"]["title"] = title feature["properties"]["title"] = title

View File

@@ -23,7 +23,7 @@ class BaseModalForm(BaseForm, BSModalForm):
""" """
is_modal_form = True is_modal_form = True
render_submit = True render_submit = True
_TEMPLATE = "modal/modal_form.html" template = "modal/modal_form.html"
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@@ -43,7 +43,7 @@ class BaseModalForm(BaseForm, BSModalForm):
""" """
redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home") redirect_url = redirect_url if redirect_url is not None else request.META.get("HTTP_REFERER", "home")
template = self._TEMPLATE template = self.template
if request.method == "POST": if request.method == "POST":
if self.is_valid(): if self.is_valid():
if not is_ajax(request.META): if not is_ajax(request.META):

View File

@@ -8,10 +8,10 @@ Created on: 15.08.22
from django import forms from django import forms
from django.db import transaction from django.db import transaction
from django.db.models.fields.files import FieldFile from django.db.models.fields.files import FieldFile
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from konova.forms.modals.base_form import BaseModalForm from konova.forms.modals.base_form import BaseModalForm
from konova.models import AbstractDocument
from konova.utils import validators from konova.utils import validators
from konova.utils.message_templates import DOCUMENT_EDITED, FILE_SIZE_TOO_LARGE, FILE_TYPE_UNSUPPORTED from konova.utils.message_templates import DOCUMENT_EDITED, FILE_SIZE_TOO_LARGE, FILE_TYPE_UNSUPPORTED
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -69,7 +69,7 @@ class NewDocumentModalForm(BaseModalForm):
} }
) )
) )
_DOCUMENT_CLS = None document_model = None
class Meta: class Meta:
abstract = True abstract = True
@@ -81,7 +81,7 @@ class NewDocumentModalForm(BaseModalForm):
self.form_attrs = { self.form_attrs = {
"enctype": "multipart/form-data", # important for file upload "enctype": "multipart/form-data", # important for file upload
} }
if not self._DOCUMENT_CLS: if not self.document_model:
raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__)) raise NotImplementedError("Unsupported document type for {}".format(self.instance.__class__))
def is_valid(self): def is_valid(self):
@@ -93,14 +93,14 @@ class NewDocumentModalForm(BaseModalForm):
# FieldFile declares that no new file has been uploaded and we do not need to check on the file again # FieldFile declares that no new file has been uploaded and we do not need to check on the file again
return super_valid return super_valid
mime_type_valid = self._DOCUMENT_CLS.is_mime_type_valid(_file) mime_type_valid = self.document_model.is_mime_type_valid(_file)
if not mime_type_valid: if not mime_type_valid:
self.add_error( self.add_error(
"file", "file",
FILE_TYPE_UNSUPPORTED FILE_TYPE_UNSUPPORTED
) )
file_size_valid = self._DOCUMENT_CLS.is_file_size_valid(_file) file_size_valid = self.document_model.is_file_size_valid(_file)
if not file_size_valid: if not file_size_valid:
self.add_error( self.add_error(
"file", "file",
@@ -115,7 +115,7 @@ class NewDocumentModalForm(BaseModalForm):
action = UserActionLogEntry.get_created_action(self.user) action = UserActionLogEntry.get_created_action(self.user)
edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document")) edited_action = UserActionLogEntry.get_edited_action(self.user, _("Added document"))
doc = self._DOCUMENT_CLS.objects.create( doc = self.document_model.objects.create(
created=action, created=action,
title=self.cleaned_data["title"], title=self.cleaned_data["title"],
comment=self.cleaned_data["comment"], comment=self.cleaned_data["comment"],
@@ -133,12 +133,10 @@ class NewDocumentModalForm(BaseModalForm):
class EditDocumentModalForm(NewDocumentModalForm): class EditDocumentModalForm(NewDocumentModalForm):
document = None document = None
_DOCUMENT_CLS = None document_model = AbstractDocument
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
doc_id = kwargs.pop("doc_id", None) self.document = kwargs.pop("document", None)
self.document = get_object_or_404(self._DOCUMENT_CLS, id=doc_id)
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Edit document") self.form_title = _("Edit document")
form_data = { form_data = {

View File

@@ -6,11 +6,10 @@ Created on: 15.08.22
""" """
from django import forms from django import forms
from django.shortcuts import get_object_or_404
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from konova.forms.modals.base_form import BaseModalForm from konova.forms.modals.base_form import BaseModalForm
from konova.models import BaseObject, Deadline from konova.models import BaseObject
class RemoveModalForm(BaseModalForm): class RemoveModalForm(BaseModalForm):
@@ -52,19 +51,9 @@ class RemoveDeadlineModalForm(RemoveModalForm):
deadline = None deadline = None
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
deadline_id = kwargs.pop("deadline_id", None) deadline = kwargs.pop("deadline", None)
self.deadline = get_object_or_404(Deadline, id=deadline_id) self.deadline = deadline
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
def save(self): def save(self):
self.instance.remove_deadline(self) self.instance.remove_deadline(self)
class RemoveDocumentModalForm(RemoveModalForm):
instance = None
_DOCUMENT_CLS = None
def __init__(self, *args, **kwargs):
document_id = kwargs.pop("doc_id", None)
super().__init__(*args, **kwargs)
self.instance = get_object_or_404(self._DOCUMENT_CLS, id=document_id)

View File

@@ -10,7 +10,6 @@ import json
from django.contrib.gis.db.models import MultiPolygonField from django.contrib.gis.db.models import MultiPolygonField
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.db import models, transaction from django.db import models, transaction
from django.db.models import Q
from django.utils import timezone from django.utils import timezone
from django.contrib.gis.geos import MultiPolygon from django.contrib.gis.geos import MultiPolygon
@@ -110,26 +109,17 @@ class Geometry(BaseResource):
objs (list): The list of objects objs (list): The list of objects
""" """
objs = [] objs = []
sets = [
# Some related data sets can be processed rather easily
regular_sets = [
self.intervention_set, self.intervention_set,
self.compensation_set,
self.ema_set, self.ema_set,
self.ecoaccount_set, self.ecoaccount_set,
] ]
for _set in regular_sets: for _set in sets:
set_objs = _set.filter( set_objs = _set.filter(
deleted=None deleted=None
) )
objs += set_objs objs += set_objs
# ... but we need a special treatment for compensations, since they can be deleted directly OR inherit their
# de-facto-deleted status from their deleted parent intervention
comp_objs = self.compensation_set.filter(
Q(deleted=None) & Q(intervention__deleted=None)
)
objs += comp_objs
return objs return objs
def get_data_object(self): def get_data_object(self):

View File

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

View File

@@ -291,5 +291,5 @@ Overwrites netgis.css attributes
} }
.netgis-menu{ .netgis-menu{
z-index: 100 !important; z-index: 1 !important;
} }

View File

@@ -11,4 +11,4 @@ BASE_TITLE = "KSP - Kompensationsverzeichnis Service Portal"
BASE_FRONTEND_TITLE = "Kompensationsverzeichnis Service Portal" BASE_FRONTEND_TITLE = "Kompensationsverzeichnis Service Portal"
TAB_TITLE_IDENTIFIER = "tab_title" TAB_TITLE_IDENTIFIER = "tab_title"
HELP_LINK = "https://dienste.naturschutz.rlp.de/doku/doku.php?id=ksp2:start" HELP_LINK = "https://dienste.naturschutz.rlp.de/doku/doku.php?id=ksp2:start"
IMPRESSUM_LINK = "https://naturschutz.rlp.de/ueber-uns/impressum" IMPRESSUM_LINK = "https://naturschutz.rlp.de/index.php?q=impressum"

View File

@@ -191,11 +191,10 @@ STATICFILES_DIRS = [
] ]
# EMAIL (see https://docs.djangoproject.com/en/dev/topics/email/) # EMAIL (see https://docs.djangoproject.com/en/dev/topics/email/)
# CHANGE_ME !!! ONLY FOR DEVELOPMENT !!!
if DEBUG: if DEBUG:
# ONLY FOR DEVELOPMENT NEEDED
EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location EMAIL_FILE_PATH = '/tmp/app-messages'
DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL") # The default email address for the 'from' element DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL") # The default email address for the 'from' element
SERVER_EMAIL = DEFAULT_FROM_EMAIL # The default email sender address, which is used by Django to send errors via mail SERVER_EMAIL = DEFAULT_FROM_EMAIL # The default email sender address, which is used by Django to send errors via mail

View File

@@ -543,7 +543,7 @@ class BaseViewTestCase(BaseTestCase):
for url, redirect_to in urls.items(): for url, redirect_to in urls.items():
response = client.get(url, follow=True) response = client.get(url, follow=True)
# Expect redirects to the landing page # Expect redirects to the landing page
self.assertEqual(response.redirect_chain[0], (redirect_to, 302), msg=f"Failed for {url}. Expected {redirect_to}") self.assertEqual(response.redirect_chain[0], (redirect_to, 302), msg=f"Failed for {url}")
def assert_url_fail(self, client: Client, urls: list): def assert_url_fail(self, client: Client, urls: list):
""" Assert for all given urls a direct 302 response """ Assert for all given urls a direct 302 response

View File

@@ -103,7 +103,7 @@ class EditDeadlineModalFormTestCase(NewDeadlineModalFormTestCase):
data, data,
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
deadline_id=self.finished_deadline.id, deadline=self.finished_deadline,
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)

View File

@@ -17,9 +17,9 @@ from django.utils.timezone import now
from compensation.forms.modals.document import NewEcoAccountDocumentModalForm, NewCompensationDocumentModalForm from compensation.forms.modals.document import NewEcoAccountDocumentModalForm, NewCompensationDocumentModalForm
from compensation.models import Payment from compensation.models import Payment
from ema.forms import NewEmaDocumentModalForm from ema.forms import NewEmaDocumentModalForm
from intervention.forms.modals.document import NewInterventionDocumentModalForm, EditInterventionDocumentModalForm from intervention.forms.modals.document import NewInterventionDocumentModalForm
from intervention.models import InterventionDocument from intervention.models import InterventionDocument
from konova.forms.modals import NewDocumentModalForm, RecordModalForm, RemoveModalForm, \ from konova.forms.modals import EditDocumentModalForm, NewDocumentModalForm, RecordModalForm, RemoveModalForm, \
RemoveDeadlineModalForm, ResubmissionModalForm RemoveDeadlineModalForm, ResubmissionModalForm
from konova.models import Resubmission from konova.models import Resubmission
from konova.tests.test_views import BaseTestCase from konova.tests.test_views import BaseTestCase
@@ -106,12 +106,12 @@ class EditDocumentModalFormTestCase(NewDocumentModalFormTestCase):
InterventionDocument, InterventionDocument,
instance=self.intervention instance=self.intervention
) )
self.form = EditInterventionDocumentModalForm( self.form = EditDocumentModalForm(
self.data, self.data,
dummy_file_dict, dummy_file_dict,
request=self.request, request=self.request,
instance=self.intervention, instance=self.intervention,
doc_id=self.doc.id document=self.doc
) )
def test_init(self): def test_init(self):
@@ -122,6 +122,7 @@ class EditDocumentModalFormTestCase(NewDocumentModalFormTestCase):
self.assertEqual(self.form.fields["title"].initial, self.doc.title) self.assertEqual(self.form.fields["title"].initial, self.doc.title)
self.assertEqual(self.form.fields["comment"].initial, self.doc.comment) self.assertEqual(self.form.fields["comment"].initial, self.doc.comment)
self.assertEqual(self.form.fields["creation_date"].initial, self.doc.date_of_creation) self.assertEqual(self.form.fields["creation_date"].initial, self.doc.date_of_creation)
self.assertEqual(self.form.fields["file"].initial, self.doc.file)
def test_save(self): def test_save(self):
self.assertTrue(self.form.is_valid(), msg=self.form.errors) self.assertTrue(self.form.is_valid(), msg=self.form.errors)
@@ -255,7 +256,7 @@ class RemoveDeadlineTestCase(BaseTestCase):
form = RemoveDeadlineModalForm( form = RemoveDeadlineModalForm(
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
deadline_id=self.finished_deadline.id deadline=self.finished_deadline
) )
self.assertEqual(form.form_title, str(_("Remove"))) self.assertEqual(form.form_title, str(_("Remove")))
self.assertEqual(form.form_caption, str(_("Are you sure?"))) self.assertEqual(form.form_caption, str(_("Are you sure?")))
@@ -272,7 +273,7 @@ class RemoveDeadlineTestCase(BaseTestCase):
data, data,
request=self.request, request=self.request,
instance=self.compensation, instance=self.compensation,
deadline_id=self.finished_deadline.id deadline=self.finished_deadline
) )
self.assertTrue(form.is_valid(), msg=form.errors) self.assertTrue(form.is_valid(), msg=form.errors)
form.save() form.save()

View File

@@ -7,7 +7,9 @@ Created on: 01.09.21
""" """
from django.http import FileResponse, HttpRequest, Http404 from django.http import FileResponse, HttpRequest, Http404
from konova.forms.modals import RemoveModalForm
from konova.models import AbstractDocument from konova.models import AbstractDocument
from konova.utils.message_templates import DOCUMENT_REMOVED_TEMPLATE
def get_document(doc: AbstractDocument): def get_document(doc: AbstractDocument):
@@ -24,3 +26,28 @@ def get_document(doc: AbstractDocument):
return FileResponse(doc.file, as_attachment=True) return FileResponse(doc.file, as_attachment=True)
except FileNotFoundError: except FileNotFoundError:
raise Http404() raise Http404()
def remove_document(request: HttpRequest, doc: AbstractDocument):
""" Renders a form for uploading new documents
This function works using a modal. We are not using the regular way, the django bootstrap modal forms are
intended to be used. Instead of View classes we work using the classic way of dealing with forms (see below).
It is important to mention, that modal forms, which should reload the page afterwards, must provide a
'reload_page' bool in the context. This way, the modal may reload the page or not.
For further details see the comments in templates/modal or
https://github.com/trco/django-bootstrap-modal-forms
Args:
request (HttpRequest): The incoming request
Returns:
"""
title = doc.title
form = RemoveModalForm(request.POST or None, instance=doc, request=request)
return form.process_request(
request=request,
msg_success=DOCUMENT_REMOVED_TEMPLATE.format(title),
)

View File

@@ -5,11 +5,6 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.09.21 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: def format_german_float(num) -> str:
@@ -24,27 +19,3 @@ def format_german_float(num) -> str:
num (str): The number as german Gleitkommazahl num (str): The number as german Gleitkommazahl
""" """
return format(num, "0,.2f").replace(",", "X").replace(".", ",").replace("X", ".") 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

View File

@@ -7,6 +7,10 @@ Created on: 09.11.20
""" """
import random import random
import string import string
import qrcode
import qrcode.image.svg
from io import BytesIO
def generate_token() -> str: def generate_token() -> str:
@@ -38,3 +42,23 @@ def generate_random_string(length: int, use_numbers: bool = False, use_letters_l
ret_val = "".join(random.choice(elements) for i in range(length)) ret_val = "".join(random.choice(elements) for i in range(length))
return ret_val return ret_val
def generate_qr_code(content: str, size: int = 20) -> str:
""" Generates a qr code from given content
Args:
content (str): The content for the qr code
size (int): The image size
Returns:
qrcode_svg (str): The qr code as svg
"""
qrcode_factory = qrcode.image.svg.SvgImage
qrcode_img = qrcode.make(
content,
image_factory=qrcode_factory,
box_size=size
)
stream = BytesIO()
qrcode_img.save(stream)
return stream.getvalue().decode()

View File

@@ -19,20 +19,7 @@ IDENTIFIER_REPLACED = _("The identifier '{}' had to be changed to '{}' since ano
ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office users are allowed to remove entries.") ENTRY_REMOVE_MISSING_PERMISSION = _("Only conservation or registration office users are allowed to remove entries.")
MISSING_GROUP_PERMISSION = _("You need to be part of another user group.") MISSING_GROUP_PERMISSION = _("You need to be part of another user group.")
CHECK_STATE_RESET = _("Status of Checked reset") CHECK_STATE_RESET = _("Status of Checked reset")
# USER | TEAM
TEAM_ADDED = _("New team added")
TEAM_EDITED = _("Team edited")
TEAM_REMOVED = _("Team removed")
TEAM_LEFT = _("Left Team")
# REMOVED
GENERIC_REMOVED_TEMPLATE = _("{} removed")
# RECORDING
RECORDED_BLOCKS_EDIT = _("Entry is recorded. To edit data, the entry first needs to be unrecorded.") RECORDED_BLOCKS_EDIT = _("Entry is recorded. To edit data, the entry first needs to be unrecorded.")
ENTRY_RECORDED = _("{} recorded")
ENTRY_UNRECORDED = _("{} unrecorded")
# SHARE # SHARE
DATA_UNSHARED = _("This data is not shared with you") DATA_UNSHARED = _("This data is not shared with you")
@@ -107,7 +94,4 @@ DATA_CHECKED_PREVIOUSLY_TEMPLATE = _("Data has changed since last check on {} by
DATA_IS_UNCHECKED = _("Current data not checked yet") DATA_IS_UNCHECKED = _("Current data not checked yet")
# API TOKEN SETTINGS # API TOKEN SETTINGS
NEW_API_TOKEN_GENERATED = _("New token generated. Administrators need to validate.") NEW_API_TOKEN_GENERATED = _("New token generated. Administrators need to validate.")
# RESUBMISSION
NEW_RESUBMISSION_CREATED = _("Resubmission set")

View File

@@ -1,47 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 17.10.25
"""
from io import BytesIO
import qrcode
import qrcode.image.svg as svg
class QrCode:
""" A wrapping class for creating a qr code with content
"""
_content = None
_img = None
def __init__(self, content: str, size: int):
self._content = content
self._img = self._generate_qr_code(content, size)
def _generate_qr_code(self, content: str, size: int = 20) -> str:
""" Generates a qr code from given content
Args:
content (str): The content for the qr code
size (int): The image size
Returns:
qrcode_svg (str): The qr code as svg
"""
img_factory = svg.SvgImage
qrcode_img = qrcode.make(
content,
image_factory=img_factory,
box_size=size
)
stream = BytesIO()
qrcode_img.save(stream)
return stream.getvalue().decode()
def get_img(self):
return self._img
def get_content(self):
return self._content

View File

@@ -5,47 +5,104 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 22.08.22 Created on: 22.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views import View
from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \ from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \
EditCompensationActionModalForm, RemoveCompensationActionModalForm EditCompensationActionModalForm, RemoveCompensationActionModalForm
from compensation.models import CompensationAction
from konova.utils.message_templates import COMPENSATION_STATE_ADDED, COMPENSATION_STATE_EDITED, \ from konova.utils.message_templates import COMPENSATION_STATE_ADDED, COMPENSATION_STATE_EDITED, \
COMPENSATION_STATE_REMOVED COMPENSATION_STATE_REMOVED
from konova.views.modal import AbstractModalFormView
class AbstractCompensationActionView(LoginRequiredMixin, AbstractModalFormView): class AbstractCompensationActionView(View):
_MODEL_CLS = None model = None
_REDIRECT_URL = None redirect_url = None
class Meta: class Meta:
abstract = True abstract = True
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
def _get_redirect_url(self, *args, **kwargs):
return super()._get_redirect_url(*args, **kwargs) + "#related_data"
class AbstractNewCompensationActionView(AbstractCompensationActionView): class AbstractNewCompensationActionView(AbstractCompensationActionView):
_FORM_CLS = NewCompensationActionModalForm
_MSG_SUCCESS = COMPENSATION_STATE_ADDED
class Meta: class Meta:
abstract = True abstract = True
def get(self, request, id: str):
""" Renders a form for adding new actions
Args:
request (HttpRequest): The incoming request
id (str): The object's id to which the new action will be related
Returns:
"""
obj = get_object_or_404(self.model, id=id)
form = NewCompensationActionModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_ADDED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str):
return self.get(request, id)
class AbstractEditCompensationActionView(AbstractCompensationActionView): class AbstractEditCompensationActionView(AbstractCompensationActionView):
_FORM_CLS = EditCompensationActionModalForm
_MSG_SUCCESS = COMPENSATION_STATE_EDITED
class Meta: class Meta:
abstract = True abstract = True
def get(self, request, id: str, action_id: str):
""" Renders a form for editing a action
Args:
request (HttpRequest): The incoming request
id (str): The object id
action_id (str): The action's id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = EditCompensationActionModalForm(request.POST or None, instance=obj, action=action, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_EDITED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str, action_id: str):
return self.get(request, id, action_id)
class AbstractRemoveCompensationActionView(AbstractCompensationActionView): class AbstractRemoveCompensationActionView(AbstractCompensationActionView):
_FORM_CLS = RemoveCompensationActionModalForm
_MSG_SUCCESS = COMPENSATION_STATE_REMOVED
class Meta: class Meta:
abstract = True abstract = True
def get(self, request, id: str, action_id: str):
""" Renders a form for removing aaction
Args:
request (HttpRequest): The incoming request
id (str): The object id
action_id (str): The action's id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
action = get_object_or_404(CompensationAction, id=action_id)
form = RemoveCompensationActionModalForm(request.POST or None, instance=obj, action=action, request=request)
return form.process_request(
request,
msg_success=COMPENSATION_STATE_REMOVED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str, action_id: str):
return self.get(request, id, action_id)

View File

@@ -1,115 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 15.10.25
"""
from abc import abstractmethod
from django.contrib import messages
from django.http import HttpRequest
from django.shortcuts import redirect
from django.urls import reverse
from django.views import View
from konova.utils.general import check_user_is_in_any_group
from konova.utils.message_templates import MISSING_GROUP_PERMISSION, DATA_UNSHARED
class AbstractBaseView(View):
""" An abstract base view
This class represents the root of all views on this project. It defines private variables which have to be used
by inheriting classes for proper generic inheriting.
"""
_TEMPLATE: str = "CHANGE_ME" # Path to template file
_TAB_TITLE: str = "CHANGE_ME" # Title displayed on browser tab
_REDIRECT_URL: str = "CHANGE_ME" # Default URL to redirect after processing (notation as django url "namespace:endpoint")
_REDIRECT_URL_ERROR: str = "home" # Default URL to redirect in case of an error (same notation)
class Meta:
abstract = True
def dispatch(self, request, *args, **kwargs):
""" Dispatching requests before forwarding them into GET or POST endpoints.
Defines basic checks which need to be done before a user can get access to any view inheriting from
this class.
Args:
request (HttpRequest): The incoming request
*args ():
**kwargs ():
Returns:
"""
request = check_user_is_in_any_group(request)
if not self._user_has_permission(request.user, **kwargs):
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)
@abstractmethod
def _user_has_permission(self, user, **kwargs):
""" Checks whether the user has permission to get this view rendered.
If no specific check is needed, this method can be overwritten with a simple True returning.
Args:
user (User): The performing user
**kwargs ():
Returns:
has_permission (bool): Whether the user has permission to see this view
"""
raise NotImplementedError("User permission not checked!")
@abstractmethod
def _user_has_shared_access(self, user, **kwargs):
""" Checks whether the user has shared access to this object.
If no shared-access-check is needed, this method can be overwritten with a simple True returning.
Args:
user (User): The performing user
**kwargs ():
Returns:
has_shared_access (bool): Whether the user has shared access
"""
raise NotImplementedError("Shared access not checked!")
def _get_redirect_url(self, *args, **kwargs):
""" Getter to construct a more specific, data dependant redirect URL
By default the method simply returns the pre-defined redirect URL.
Args:
*args ():
**kwargs ():
Returns:
url (str): Reversed redirect url
"""
return self._REDIRECT_URL
def _get_redirect_url_error(self, *args, **kwargs):
""" Getter to construct a more specific, data dependant redirect URL in error cases
By default the method simply returns the pre-defined redirect URL for errors.
Args:
*args ():
**kwargs ():
Returns:
url (str): Reversed redirect url
"""
return self._REDIRECT_URL_ERROR

View File

@@ -5,57 +5,102 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 22.08.22 Created on: 22.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views import View
from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm from compensation.forms.modals.deadline import NewDeadlineModalForm, EditDeadlineModalForm
from konova.forms.modals import RemoveDeadlineModalForm from konova.forms.modals import RemoveDeadlineModalForm
from konova.models import Deadline
from konova.utils.message_templates import DEADLINE_ADDED, DEADLINE_EDITED, DEADLINE_REMOVED from konova.utils.message_templates import DEADLINE_ADDED, DEADLINE_EDITED, DEADLINE_REMOVED
from konova.views.modal import AbstractModalFormView
class AbstractNewDeadlineView(LoginRequiredMixin, AbstractModalFormView): class AbstractNewDeadlineView(View):
_MODEL_CLS = None model = None
_FORM_CLS = NewDeadlineModalForm redirect_url = None
_REDIRECT_URL = None
_MSG_SUCCESS = DEADLINE_ADDED
class Meta: class Meta:
abstract = True abstract = True
def _get_redirect_url(self, *args, **kwargs): def get(self, request, id: str):
return super()._get_redirect_url(*args, **kwargs) + "#related_data" """ Renders a form for adding new deadlines
def _user_has_permission(self, user, **kwargs): Args:
return user.is_default_user() request (HttpRequest): The incoming request
id (str): The account's id to which the new state will be related
Returns:
"""
obj = get_object_or_404(self.model, id=id)
form = NewDeadlineModalForm(request.POST or None, instance=obj, request=request)
return form.process_request(
request,
msg_success=DEADLINE_ADDED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str):
return self.get(request, id)
class AbstractEditDeadlineView(LoginRequiredMixin, AbstractModalFormView): class AbstractEditDeadlineView(View):
_MODEL_CLS = None model = None
_FORM_CLS = EditDeadlineModalForm redirect_url = None
_REDIRECT_URL = None
_MSG_SUCCESS = DEADLINE_EDITED
class Meta: class Meta:
abstract = True abstract = True
def _get_redirect_url(self, *args, **kwargs): def get(self, request, id: str, deadline_id: str):
return super()._get_redirect_url(*args, **kwargs) + "#related_data" """ Renders a form for editing deadlines
def _user_has_permission(self, user, **kwargs): Args:
return user.is_default_user() request (HttpRequest): The incoming request
id (str): The compensation's id
deadline_id (str): The deadline's id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id)
form = EditDeadlineModalForm(request.POST or None, instance=obj, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_EDITED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str, deadline_id: str):
return self.get(request, id, deadline_id)
class AbstractRemoveDeadlineView(LoginRequiredMixin, AbstractModalFormView): class AbstractRemoveDeadlineView(View):
_MODEL_CLS = None model = None
_FORM_CLS = RemoveDeadlineModalForm redirect_url = None
_REDIRECT_URL = None
_MSG_SUCCESS = DEADLINE_REMOVED
class Meta: class Meta:
abstract = True abstract = True
def _get_redirect_url(self, *args, **kwargs): def get(self, request, id: str, deadline_id: str):
return super()._get_redirect_url(*args, **kwargs) + "#related_data" """ Renders a form for removing deadlines
def _user_has_permission(self, user, **kwargs): Args:
return user.is_default_user() request (HttpRequest): The incoming request
id (str): The compensation's id
deadline_id (str): The deadline's id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
deadline = get_object_or_404(Deadline, id=deadline_id)
form = RemoveDeadlineModalForm(request.POST or None, instance=obj, deadline=deadline, request=request)
return form.process_request(
request,
msg_success=DEADLINE_REMOVED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str, deadline_id: str):
return self.get(request, id, deadline_id)

View File

@@ -6,88 +6,126 @@ Created on: 22.08.22
""" """
from django.core.exceptions import ObjectDoesNotExist 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.urls import reverse
from django.views import View
from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \ from intervention.forms.modals.deduction import NewEcoAccountDeductionModalForm, EditEcoAccountDeductionModalForm, \
RemoveEcoAccountDeductionModalForm RemoveEcoAccountDeductionModalForm
from konova.utils.general import check_id_is_valid_uuid from konova.utils.message_templates import DEDUCTION_ADDED, DEDUCTION_EDITED, DEDUCTION_REMOVED, DEDUCTION_UNKNOWN
from konova.views.modal import AbstractModalFormView
class AbstractDeductionView(AbstractModalFormView): class AbstractDeductionView(View):
_REDIRECT_URL = None model = None
redirect_url = None
def dispatch(self, request, *args, **kwargs):
check_id_is_valid_uuid(kwargs.get("id"))
return super().dispatch(request, *args, **kwargs)
def _custom_check(self, obj): def _custom_check(self, obj):
""" """
Can be used by inheriting classes to provide custom checks before further processing Can be used by inheriting classes to provide custom checks before further processing
""" """
pass raise NotImplementedError("Must be implemented in subclasses")
def _user_has_permission(self, user, **kwargs) -> 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"
class AbstractNewDeductionView(AbstractDeductionView): class AbstractNewDeductionView(AbstractDeductionView):
_FORM_CLS = NewEcoAccountDeductionModalForm
class Meta: class Meta:
abstract = True 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): class AbstractEditDeductionView(AbstractDeductionView):
_FORM_CLS = EditEcoAccountDeductionModalForm
def dispatch(self, request, *args, **kwargs): def _custom_check(self, obj):
check_id_is_valid_uuid(kwargs.get("deduction_id")) pass
return super().dispatch(request, *args, **kwargs)
class Meta: class Meta:
abstract = True 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): class AbstractRemoveDeductionView(AbstractDeductionView):
_FORM_CLS = RemoveEcoAccountDeductionModalForm
def dispatch(self, request, *args, **kwargs): def _custom_check(self, obj):
check_id_is_valid_uuid(kwargs.get("deduction_id")) pass
return super().dispatch(request, *args, **kwargs)
class Meta: class Meta:
abstract = True 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)

View File

@@ -1,107 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 17.10.25
"""
from abc import abstractmethod
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest
from django.shortcuts import render
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 AbstractBaseView
class BaseDetailView(LoginRequiredMixin, AbstractBaseView):
_MODEL_CLS = None
class Meta:
abstract = True
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, **kwargs):
# 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

View File

@@ -5,35 +5,46 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 22.08.22 Created on: 22.08.22
""" """
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.urls import reverse
from django.views import View
from konova.forms.modals import EditDocumentModalForm from konova.utils.documents import get_document, remove_document
from konova.utils.documents import get_document from konova.utils.message_templates import DOCUMENT_ADDED, DOCUMENT_EDITED
from konova.utils.message_templates import DOCUMENT_ADDED, DOCUMENT_EDITED, DOCUMENT_REMOVED_TEMPLATE
from konova.views.modal import AbstractModalFormView, AbstractBaseView
class AbstractNewDocumentView(LoginRequiredMixin, AbstractModalFormView): class AbstractNewDocumentView(View):
_MODEL_CLS = None model = None
_FORM_CLS = None form = None
_REDIRECT_URL = None redirect_url = None
_MSG_SUCCESS = DOCUMENT_ADDED
class Meta: class Meta:
abstract = True abstract = True
def _get_redirect_url(self, *args, **kwargs): def get(self, request, id: str):
return super()._get_redirect_url(*args, **kwargs) + "#related_data" """ Renders a form for uploading new documents
def _user_has_permission(self, user, **kwargs): Args:
return user.is_default_user() request (HttpRequest): The incoming request
id (str): The object's id to which the new document will be related
Returns:
"""
intervention = get_object_or_404(self.model, id=id)
form = self.form(request.POST or None, request.FILES or None, instance=intervention, request=request)
return form.process_request(
request,
msg_success=DOCUMENT_ADDED,
redirect_url=reverse(self.redirect_url, args=(id,)) + "#related_data"
)
def post(self, request, id: str):
return self.get(request, id)
class AbstractGetDocumentView(LoginRequiredMixin, AbstractBaseView): class AbstractGetDocumentView(View):
_MODEL_CLS = None model = None
_DOCUMENT_CLS = None document_model = None
class Meta: class Meta:
abstract = True abstract = True
@@ -51,57 +62,77 @@ class AbstractGetDocumentView(LoginRequiredMixin, AbstractBaseView):
Returns: Returns:
""" """
get_object_or_404(self._MODEL_CLS, id=id) get_object_or_404(self.model, id=id)
doc = get_object_or_404(self._DOCUMENT_CLS, id=doc_id) doc = get_object_or_404(self.document_model, id=doc_id)
return get_document(doc) return get_document(doc)
def post(self, request, id: str, doc_id: str): def post(self, request, id: str, doc_id: str):
return self.get(request, id, doc_id) return self.get(request, id, doc_id)
def _user_has_permission(self, user, **kwargs):
return user.is_default_user()
def _user_has_shared_access(self, user, **kwargs): class AbstractRemoveDocumentView(View):
obj = kwargs.get("id", None) model = None
assert obj is not None document_model = None
obj = get_object_or_404(self._MODEL_CLS, id=obj)
return obj.is_shared_with(user)
class AbstractRemoveDocumentView(LoginRequiredMixin, AbstractModalFormView):
_MODEL_CLS = None
_DOCUMENT_CLS = None
_FORM_CLS = None
_MSG_SUCCESS = DOCUMENT_REMOVED_TEMPLATE
class Meta: class Meta:
abstract = True abstract = True
def _get_redirect_url(self, *args, **kwargs): def get(self, request, id: str, doc_id: str):
return super()._get_redirect_url(*args, **kwargs) + "#related_data" """ Removes the document from the database and file system
def _user_has_permission(self, user, **kwargs): Wraps the generic functionality from konova.utils.
return user.is_default_user()
def _get_msg_success(self, *args, **kwargs): Args:
doc_id = kwargs.get("doc_id", None) request (HttpRequest): The incoming request
assert doc_id is not None id (str): The intervention id
doc = get_object_or_404(self._DOCUMENT_CLS, id=doc_id) doc_id (str): The document id
return self._MSG_SUCCESS.format(doc.title)
Returns:
"""
get_object_or_404(self.model, id=id)
doc = get_object_or_404(self.document_model, id=doc_id)
return remove_document(
request,
doc
)
def post(self, request, id: str, doc_id: str):
return self.get(request, id, doc_id)
class AbstractEditDocumentView(LoginRequiredMixin, AbstractModalFormView): class AbstractEditDocumentView(View):
_MODEL_CLS = None model = None
_DOCUMENT_CLS = None document_model = None
_FORM_CLS = EditDocumentModalForm form = None
_REDIRECT_URL = None redirect_url = None
_MSG_SUCCESS = DOCUMENT_EDITED
class Meta: class Meta:
abstract = True abstract = True
def _user_has_permission(self, user, **kwargs): def get(self, request, id: str, doc_id: str):
return user.is_default_user() """ GET handling for editing of existing document
Wraps the generic functionality from konova.utils.
Args:
request (HttpRequest): The incoming request
id (str): The intervention id
doc_id (str): The document id
Returns:
"""
obj = get_object_or_404(self.model, id=id)
doc = get_object_or_404(self.document_model, id=doc_id)
form = self.form(request.POST or None, request.FILES or None, instance=obj, document=doc,
request=request)
return form.process_request(
request,
DOCUMENT_EDITED,
redirect_url=reverse(self.redirect_url, args=(obj.id,)) + "#related_data"
)
def post(self, request, id: str, doc_id: str):
return self.get(request, id, doc_id)
def _get_redirect_url(self, *args, **kwargs):
return super()._get_redirect_url(*args, **kwargs) + "#related_data"

View File

@@ -1,282 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 12.12.25
"""
from django.contrib import messages
from django.contrib.auth.mixins import LoginRequiredMixin
from django.http import HttpRequest
from django.shortcuts import get_object_or_404, redirect, render
from django.urls import reverse
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.message_templates import RECORDED_BLOCKS_EDIT, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE, \
FORM_INVALID, IDENTIFIER_REPLACED
from konova.views.base import AbstractBaseView
class AbstractFormView(AbstractBaseView):
""" Abstract base class for rendering form views
"""
_MODEL_CLS = None
_FORM_CLS = None
class Meta:
abstract = True
def _get_additional_context(self, **kwargs):
""" Getter for additional data, which is needed to properly render the current view
Args:
**kwargs ():
Returns:
context (dict): Additional context data for rendering
"""
return {}
class AbstractGeometryFormView(LoginRequiredMixin, AbstractFormView):
""" Abstract base view for processing objects with spatial data
"""
_GEOMETRY_FORM_CLS = SimpleGeomForm
class Meta:
abstract = True
class AbstractNewGeometryFormView(AbstractGeometryFormView):
""" Base view for creating new spatial data related to objects
"""
def _user_has_permission(self, user, **kwargs):
# 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, *args, **kwargs):
""" GET endpoint for rendering a form view where object data and spatial data are processed
Args:
request (HttpRequest): The incoming request
**kwargs ():
Returns:
"""
# First initialize the regular object form and the geometry form based on request-bound data
form: BaseForm = self._FORM_CLS(None, **kwargs, user=request.user)
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, user=request.user, read_only=False)
# Get some additional context and put everything into the rendering pipeline
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, *args, **kwargs):
""" POST endpoint for processing object and spatial data provided by forms
Args:
request (HttpRequest): The incoming request
**kwargs ():
Returns:
"""
# First initialize the regular object form and the geometry form based on request-bound data
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)
# Only continue if both forms are without errors
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)
# There is a rare chance that an identifier has been taken already between sending the form and processing
# the data. If the identifier can not be used anymore, we have to inform the user that another identifier
# had to be generated
if generated_identifier != obj.identifier:
messages.info(
request,
IDENTIFIER_REPLACED.format(
generated_identifier,
obj.identifier
)
)
messages.success(request, _("{} added").format(obj.identifier))
# Very complex geometries have to be simplified automatically while processing the spatial data. If this
# is the case, the user has to be informed. (They might want to check whether the stored geometry still
# fits their needs)
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
# If certain parts of the geometry do not pass the quality check (e.g. way too small and therefore more like
# cutting errors) we need to inform the user that some parts have been removed/ignored while storing the
# geometry
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:
# Something was not properly entered on the forms, so we have to inform the user
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 AbstractEditGeometryFormView(AbstractGeometryFormView):
""" Base view for editing new spatial data related to objects
"""
_TAB_TITLE = _("Edit {}")
def get(self, request: HttpRequest, id: str, *args, **kwargs):
""" GET endpoint for rendering a form view where object data and spatial data are processed
Args:
request (HttpRequest): The incoming request
id (str): The id of the object (not the geometry)
Returns:
"""
# First fetch the object identified by the id
obj = get_object_or_404(
self._MODEL_CLS,
id=id
)
obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,))
# Check whether the object is recorded. If so - we can redirect the user and inform about the un-editability
# of this entry
if obj.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect(obj_redirect_url)
# Seems like the object is not recorded. Good - initialize the forms based on the obj and request-bound data
form: BaseForm = self._FORM_CLS(None, instance=obj, user=request.user)
geom_form: SimpleGeomForm = self._GEOMETRY_FORM_CLS(None, instance=obj, read_only=False)
# Get additional context for rendering and put everything in the rendering pipeline
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, *args, **kwargs):
""" POST endpoint for processing object and spatial data provided by forms
Args:
request (HttpRequest): The incoming request
id (str): The object's id
*args ():
**kwargs ():
Returns:
"""
obj = get_object_or_404(
self._MODEL_CLS,
id=id
)
obj_redirect_url = reverse(self._REDIRECT_URL, args=(obj.id,))
# If the object is recorded, we abort the processing directly and inform the user
if obj.is_recorded:
messages.info(
request,
RECORDED_BLOCKS_EDIT
)
return redirect(obj_redirect_url)
# Initialize forms with obj and request-bound data
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))
# Very complex geometries have to be simplified automatically while processing the spatial data. If this
# is the case, the user has to be informed. (They might want to check whether the stored geometry still
# fits their needs)
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
# If certain parts of the geometry do not pass the quality check (e.g. way too small and therefore more like
# cutting errors) we need to inform the user that some parts have been removed/ignored while storing the
# geometry
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, **kwargs):
return user.is_default_user()

View File

@@ -10,16 +10,15 @@ from django.http import HttpResponse, HttpRequest
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils import timezone from django.utils import timezone
from django.views import View
from konova.models import Geometry from konova.models import Geometry
from konova.settings import GEOM_THRESHOLD_RECALCULATION_SECONDS from konova.settings import GEOM_THRESHOLD_RECALCULATION_SECONDS
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.tasks import celery_update_parcels from konova.tasks import celery_update_parcels
from konova.views.base import AbstractBaseView
class GeomParcelsView(AbstractBaseView): class GeomParcelsView(View):
_TEMPLATE = "konova/includes/parcels/parcel_table_frame.html"
def get(self, request: HttpRequest, id: str): def get(self, request: HttpRequest, id: str):
""" Getter for HTMX """ Getter for HTMX
@@ -33,6 +32,7 @@ class GeomParcelsView(AbstractBaseView):
Returns: Returns:
A rendered piece of HTML A rendered piece of HTML
""" """
template = "konova/includes/parcels/parcel_table_frame.html"
geom = get_object_or_404(Geometry, id=id) geom = get_object_or_404(Geometry, id=id)
geos_geom = geom.geom or MultiPolygon(srid=DEFAULT_SRID_RLP) geos_geom = geom.geom or MultiPolygon(srid=DEFAULT_SRID_RLP)
@@ -85,7 +85,7 @@ class GeomParcelsView(AbstractBaseView):
"geom_id": str(id), "geom_id": str(id),
"next_page": next_page, "next_page": next_page,
} }
html = render_to_string(self._TEMPLATE, context, request) html = render_to_string(template, context, request)
return HttpResponse(html, status=status_code) return HttpResponse(html, status=status_code)
else: else:
return HttpResponse(None, status=404) return HttpResponse(None, status=404)
@@ -107,15 +107,8 @@ class GeomParcelsView(AbstractBaseView):
waiting_too_long = (pcs_diff >= wait_for_seconds) waiting_too_long = (pcs_diff >= wait_for_seconds)
return waiting_too_long return waiting_too_long
def _user_has_shared_access(self, user, **kwargs):
return True
def _user_has_permission(self, user, **kwargs): class GeomParcelsContentView(View):
return True
class GeomParcelsContentView(AbstractBaseView):
_TEMPLATE = "konova/includes/parcels/parcel_table_content.html"
def get(self, request: HttpRequest, id: str, page: int): def get(self, request: HttpRequest, id: str, page: int):
""" Getter for infinite scroll of HTMX """ Getter for infinite scroll of HTMX
@@ -137,6 +130,7 @@ class GeomParcelsContentView(AbstractBaseView):
# HTTP code 286 states that the HTMX should stop polling for updates # HTTP code 286 states that the HTMX should stop polling for updates
# https://htmx.org/docs/#polling # https://htmx.org/docs/#polling
status_code = 286 status_code = 286
template = "konova/includes/parcels/parcel_table_content.html"
geom = get_object_or_404(Geometry, id=id) geom = get_object_or_404(Geometry, id=id)
parcels = geom.get_underlying_parcels() parcels = geom.get_underlying_parcels()
@@ -154,11 +148,5 @@ class GeomParcelsContentView(AbstractBaseView):
"geom_id": str(id), "geom_id": str(id),
"next_page": next_page, "next_page": next_page,
} }
html = render_to_string(self._TEMPLATE, context, request) html = render_to_string(template, context, request)
return HttpResponse(html, status=status_code) return HttpResponse(html, status=status_code)
def _user_has_shared_access(self, user, **kwargs):
return True
def _user_has_permission(self, user, **kwargs):
return True

View File

@@ -9,19 +9,21 @@ from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q from django.db.models import Q
from django.http import HttpRequest from django.http import HttpRequest
from django.shortcuts import render from django.shortcuts import render
from django.utils.decorators import method_decorator
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.views import View
from compensation.models import EcoAccount, Compensation from compensation.models import EcoAccount, Compensation
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import any_group_check
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.views.base import AbstractBaseView
from news.models import ServerMessage from news.models import ServerMessage
class HomeView(LoginRequiredMixin, AbstractBaseView): class HomeView(LoginRequiredMixin, View):
_TEMPLATE = "konova/home.html"
@method_decorator(any_group_check)
def get(self, request: HttpRequest): def get(self, request: HttpRequest):
""" """
Renders the landing page Renders the landing page
@@ -32,6 +34,7 @@ class HomeView(LoginRequiredMixin, AbstractBaseView):
Returns: Returns:
A redirect A redirect
""" """
template = "konova/home.html"
user = request.user user = request.user
user_teams = user.shared_teams user_teams = user.shared_teams
@@ -72,12 +75,5 @@ class HomeView(LoginRequiredMixin, AbstractBaseView):
TAB_TITLE_IDENTIFIER: _("Home"), TAB_TITLE_IDENTIFIER: _("Home"),
} }
context = BaseContext(request, additional_context).context context = BaseContext(request, additional_context).context
return render(request, self._TEMPLATE, context) return render(request, template, context)
def _user_has_permission(self, user, **kwargs):
# 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

View File

@@ -1,55 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 12.12.25
"""
from django.http import HttpRequest, JsonResponse
from konova.views.base import AbstractBaseView
class AbstractIdentifierGeneratorView(AbstractBaseView):
""" View class
Process a request for generating a new identifier
"""
_MODEL_CLS = None
_REDIRECT_URL: str = "home"
class Meta:
abstract = True
def get(self, request: HttpRequest):
""" GET endpoint
Args:
request ():
Returns:
"""
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, **kwargs):
""" 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

View File

@@ -1,65 +0,0 @@
"""
Author: Michel Peltriaux
Created on: 12.12.25
"""
from abc import abstractmethod
from django.http import HttpRequest
from django.shortcuts import render
from konova.contexts import BaseContext
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.views.base import AbstractBaseView
class AbstractIndexView(AbstractBaseView):
""" Abstract base class for all index views
"""
_TEMPLATE: str = "generic_index.html"
_INDEX_TABLE_CLS = None
_REDIRECT_URL: str = "home"
class Meta:
abstract = True
def get(self, request: HttpRequest, *args, **kwargs):
""" GET endpoint for rendering index views
Args:
request (HttpRequest): The incoming request
*args ():
**kwargs ():
Returns:
"""
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):
""" Generic getter for the queryset of objects which shall be processed on this view
Returns:
"""
raise NotImplementedError
def _user_has_permission(self, user, **kwargs):
# 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

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