Compare commits

..

401 Commits
v0.5 ... master

Author SHA1 Message Date
faf8aed777 Merge pull request '# Drop django-simple-sso' (#468) from 467_Remove_django-simple-sso into master
Reviewed-on: #468
2025-01-24 16:12:06 +01:00
94c498866f # Drop django-simple-sso
* drops django-simple-sso package from project
* drops unused messenger.py
2025-01-24 16:11:23 +01:00
616965c890 Merge pull request 'bugfix' (#465) from bugfix into master
Reviewed-on: #465
2025-01-21 13:43:46 +01:00
e39c7eb51f # KSP Token optimization
* adds support for standardized bearer token usage instead of ksptoken/kspuser header usage (still supported)
2025-01-21 13:38:37 +01:00
19bd408fbd # Bugfix code update
* fixes bug where empty short names were not resolved properly
2025-01-21 12:52:46 +01:00
7bfe6a37f8 Merge pull request '# 456 Rework API key creation' (#462) from 456_Rework_API_key_creation into master
Reviewed-on: #462
2025-01-08 16:04:12 +01:00
9b63307f01 # 456 Rework API key creation
* removes frontend input field holding generated API key
* replaces with modal form
* reworks tests on API token form
2025-01-08 16:03:26 +01:00
123a470006 Merge pull request '# 457 User autocomplete fix' (#461) from 457_User_autocomplete_fix into master
Reviewed-on: #461
2025-01-08 14:36:01 +01:00
5a0c5285e7 # 457 User autocomplete fix
* fixes bug where empty query parameter would show users in autocomplete share view
* fixes same behaviour on autocomplete share team view
2025-01-08 14:35:35 +01:00
3a01eaff92 Merge pull request '# 450 Optimization recalculate_parcels command' (#460) from 450_Optimierung_Kommando_Nachverschneidung into master
Reviewed-on: #460
2025-01-08 14:27:55 +01:00
2af91aa178 # 450 Optimization recalculate_parcels command
* optimizes recalculate_parcels.py command so that only non-empty geometries will be processed
* drops test_identifier_generating.py command due to missing usage
2025-01-08 14:27:23 +01:00
53d0af89ac Merge pull request '# Hotfix' (#458) from oauth_fix into master
Reviewed-on: #458
2024-12-23 13:41:53 +01:00
7b5c1f0d97 # Hotfix
* fixes bug where anonymous user trying to logout would throw error
2024-12-23 13:41:25 +01:00
ef076c0b3b Merge pull request 'oauth_fix' (#453) from oauth_fix into master
Reviewed-on: #453
2024-12-23 12:09:20 +01:00
72a5075f3b # Update dependencies
* updates requirements.txt
2024-12-23 12:03:15 +01:00
d677ac6b5a # Map proxy enhancement
* adds whitelisting for map proxy hosts
2024-12-23 11:08:41 +01:00
9149e4cbd3 # Propagation improvement
* fixes documentation and variable names on oauth token revocation
* introduces private key for propagation
* changes key usage in decryption of propagated user data from oauth_client_id to private propagation key
2024-12-23 10:45:08 +01:00
1c24cbea26 # OAuth Token revocation
* adds revocation of user tokens on logout
2024-12-23 09:26:14 +01:00
fa89bbba99 Merge pull request '# Bugfix: Recalculate_parcels command' (#448) from bugfixing into master
Reviewed-on: #448
2024-11-13 16:09:22 +01:00
78eb711057 # Bugfix: Recalculate_parcels command
* fixes a bug on recalculate_parcels if not --force-all is used
2024-11-13 16:08:36 +01:00
416ad8478c Merge pull request '439_Wartungskommando_Nachverschneidung' (#446) from 439_Wartungskommando_Nachverschneidung into master
Reviewed-on: #446
2024-10-26 10:24:50 +02:00
6b28c4ec15 # Drop atomic transaction
* drops atomic transaction processing on Parcel.make_unique
2024-10-26 10:24:10 +02:00
46a2a4ff46 # Parcel recalculation optimization
* enhances workflow for parcel recalculation
2024-10-26 10:17:09 +02:00
90e5cf5b36 Merge pull request '# Parcel duplicate repair' (#444) from 439_Wartungskommando_Nachverschneidung into master
Reviewed-on: #444
2024-10-26 09:48:32 +02:00
50f46e319c # Parcel duplicate repair
* adds mechanic to repair parcels in case of unwanted parcel duplicates
* optimizes filtering of geometries for parcel recalculation
2024-10-26 09:47:27 +02:00
e2ea087c4e Merge pull request '# Wartungskommando Optimization' (#442) from 439_Wartungskommando_Nachverschneidung into master
Reviewed-on: #442
2024-10-25 19:27:02 +02:00
a6e43b044b # Wartungskommando Optimization
* extends filtering for recalculatable geometries to records without started calculation at any point (parcel_update_start is null)
* catches exceptions on geometries which could not be recalculated properly, adds them to output for further analysis
* simplifies complexity factor calculation
2024-10-25 19:19:08 +02:00
be0d261e81 Merge pull request '# 439 Wartungskommando Nachverschneidung' (#440) from 439_Wartungskommando_Nachverschneidung into master
Reviewed-on: #440
2024-10-25 14:24:18 +02:00
62e1b046c3 # 439 Wartungskommando Nachverschneidung
* refactors command update_all_parcels into recalculate_parcels
* fixes bug in command generate_report
2024-10-25 14:23:21 +02:00
669a12410f Merge pull request 'missing_migrations' (#437) from missing_migrations into master
Reviewed-on: #437
2024-08-26 18:57:10 +02:00
dd77e6c16e Merge branch 'refs/heads/master' into missing_migrations 2024-08-26 18:53:11 +02:00
33774ce557 # Migrations
* adds missing migrations
* renames variables shadowing in-builts
2024-08-26 18:51:58 +02:00
dc3dc99b3d Merge pull request '# User filtering' (#435) from 433_Filter_by_user into master
Reviewed-on: #435
2024-08-19 11:42:18 +02:00
315f9de958 # User filtering
* adds query filter to search for logged users on entries
2024-08-19 11:38:09 +02:00
0726c15086 Merge pull request '432_Unreadable_payments' (#434) from 432_Unreadable_payments into master
Reviewed-on: #434
2024-08-19 10:26:40 +02:00
2492a8abe8 # Codelist migration optimization
* adds boolean to de-/activate migration logic inside of 0002_migrate_975_to_288.py
2024-08-19 10:23:05 +02:00
dbc5cba5d7 # Variable refactoring
* renames variable `has_access` into `is_entry_shared` for better understanding in various places (mostly html related)
2024-08-19 09:44:45 +02:00
c8948ddaea # Censor payments
* censor payments if entry is not shared with user
* updates translations
2024-08-19 09:39:58 +02:00
5039da28aa Merge pull request '# Hotfix' (#430) from 427_Integration_of_codelist_288 into master
Reviewed-on: #430
2024-08-07 12:07:04 +02:00
4567339570 # Hotfix
* fixes requirements dependency
2024-08-07 12:06:27 +02:00
768849e646 Merge pull request '427_Integration_of_codelist_288' (#428) from 427_Integration_of_codelist_288 into master
Reviewed-on: #428
2024-08-07 12:01:46 +02:00
ebf10645fc # Requirements update
* updates some packages in requirements.txt
2024-08-07 09:17:20 +02:00
df241747cf # Migration list 975->288
* adds migration for app codelist to migrate existing biotope type details codes from list 975 to 288 depending on their atomID
* improves rendering of action and biotope type details on frontend for KOM, OEK and EMA
* refactors KonovaCode str() rendering
2024-08-07 09:12:38 +02:00
94e9035e10 # Codelist 288
* introduces 288 to codelist/settings.py
* refactors usage from 975 to 288
* enhances rendering of codelist names depending on which name exists (short vs long)
2024-08-06 15:39:01 +02:00
d4d39689cc Merge pull request '# Bugfix' (#425) from 424_Archived_codes_selectable into master
Reviewed-on: #425
2024-08-06 14:28:16 +02:00
2fde3f0fa3 # Bugfix
* fixes bug where archived codes has been selectable due to recursive building of child-parent hierarchy
2024-08-06 14:27:38 +02:00
b62c2e92c9 Merge pull request '# Dependency fix' (#422) from 419_Dependency_upgrade into master
Reviewed-on: #422
2024-07-10 09:30:14 +02:00
1c0fb801e6 # Dependency fix
* fixes dependency mismatch between requests 2.32.3 and kombu (requires < 2.32.0)
2024-07-10 09:29:54 +02:00
a1acff5e90 Merge pull request '# Dependency update' (#420) from 419_Dependency_upgrade into master
Reviewed-on: #420
2024-07-10 09:26:35 +02:00
25a92f59aa # Dependency update
* updates dependencies due to important version changes
2024-07-10 09:26:18 +02:00
884db6f014 Merge pull request '# Readonly map' (#417) from map_client_update_08-07-2024 into master
Reviewed-on: #417
2024-07-08 18:44:00 +02:00
59b7f3c69a # Readonly map
* fixes bug where readonly mode of map client was not readonly at all
2024-07-08 06:59:15 +02:00
0446d50438 Merge pull request '# .env fix' (#415) from env_fix into master
Reviewed-on: #415
2024-07-05 10:51:25 +02:00
12f78c85bf # .env fix
* adds celery setting to .env.sample
2024-07-05 10:50:25 +02:00
78485a4506 Merge pull request '# Requirements update' (#413) from fix_sso into master
Reviewed-on: #413
2024-07-04 11:42:05 +02:00
21a5c84b18 # Requirements update
* due to existing migrations, django-simple-sso needs to be added as a dependency as well as itsdangerous (dependency of django-simple-sso)
    * however, there is no active usage of any of these packages anymore
2024-07-04 11:39:17 +02:00
a93f509d51 Merge pull request 'env' (#411) from env into master
Reviewed-on: #411
2024-07-04 08:36:04 +02:00
38967da201 Merge pull request '407_Drop_django-simple-sso' (#410) from 407_Drop_django-simple-sso into master
Reviewed-on: #410
2024-07-04 07:58:15 +02:00
60d749db2d # Geopackage import configuration
* corrects config for geopackage import support
2024-07-04 07:44:08 +02:00
dff577309e Merge pull request '# Send-to-EGON cmd' (#408) from sending_to_egon_cmd into master
Reviewed-on: #408
2024-06-18 11:49:35 +02:00
ea590d0868 # Send-to-EGON cmd
* adds new custom command send_to_egon for performing EGON sending on a list of intervention ids
2024-06-18 11:48:56 +02:00
e09c15bd51 # Updates sso
* adds env usage for sso settings
2024-06-14 13:04:25 +02:00
c3019f83fd Merge branch 'refs/heads/407_Drop_django-simple-sso' into env
# Conflicts:
#	konova/sub_settings/sso_settings.py
#	requirements.txt
2024-06-14 13:02:33 +02:00
93a71a7055 # Requirements update
* updates requirements.txt
* drops django-simple-sso from codebase and requirements.txt
2024-06-14 13:00:09 +02:00
35b1409359 # Requirements update
* updates requirements.txt
* drops debug-toolbar
2024-06-14 07:42:17 +02:00
c9aeb393b5 Merge pull request '# Comment card' (#406) from comment_card_improvement into master
Reviewed-on: #406
2024-05-21 14:43:49 +02:00
6df46e7642 # Comment card
* adds proper line break rendering in comment card
2024-05-21 14:42:49 +02:00
fe366bc568 Merge pull request '# 404 Extend API' (#405) from 404_Extend_API_shared_acces into master
Reviewed-on: #405
2024-05-21 11:55:20 +02:00
a9f04a28c1 # 404 Extend API
* extends API shared record access with team based sharing
2024-05-21 11:54:06 +02:00
8c9f4888dd Merge pull request '# OAuth fix' (#402) from oauth_https_fix into master
Reviewed-on: #402
2024-05-17 10:59:02 +02:00
5c727b2eaa # OAuth fix
* fixes bug in deployment environment due to http/s usage in url
2024-05-17 10:56:33 +02:00
76b2a78fe2 Merge pull request '# Fix' (#400) from oauth_https_fix into master
Reviewed-on: #400
2024-05-17 07:54:02 +02:00
86db08fca0 # Fix
* fixes bug where oauth requests did not use https in dockered deployment environment
2024-05-17 07:49:46 +02:00
fe1dce6440 Merge pull request '# Hotfix' (#398) from 395_OAuth2_refactoring into master
Reviewed-on: #398
2024-05-16 17:37:38 +02:00
a5e6f5a1db # Hotfix
* changes randomly created code verifier into static one to avoid authentication conflicts on multi process deployment (where each process generates an own verifier...)
2024-05-16 17:37:19 +02:00
78e9cbab71 Merge pull request '395_OAuth2_refactoring' (#396) from 395_OAuth2_refactoring into master
Reviewed-on: #396
2024-05-16 15:19:19 +02:00
572348f9f1 # OAuth Propagation
* adds user propagation without django-simple-sso
2024-05-10 10:40:19 +02:00
8ff3cb9adc # OAuth migrations
* adds migrations for storing OAuthToken
* adds OAuthToken model
* adds OAuthToken admin
* adds user migration for Fkey relation to OAuthToken
2024-04-30 14:56:48 +02:00
f135008447 # OAuth refactoring code
* refactors code
2024-04-29 12:27:07 +02:00
94b7f3ad70 # OAuth requirements
* updates requirements.txt
2024-04-29 12:14:15 +02:00
d69bab36da # WIP: OAuth draft implementation
* first working client implementation of oauth workflow for logging in users
2024-04-29 12:07:06 +02:00
fa86cc142f Merge pull request 'requirements_update' (#394) from requirements_update into master
Reviewed-on: SGD-Nord/konova#394
2024-04-12 08:08:00 +02:00
6523891703 # Itsdangerous update
* adds itsdangerous package update
2024-04-12 07:51:18 +02:00
18f590f4a6 # Requirements update
* updates requirements.txt
2024-04-12 07:51:17 +02:00
b441518334 # Env
* updates env.sample
2024-04-03 13:45:52 +02:00
1a80912960 # Environment
* refactors settings into env usage
* adds proxy usage for schneider parcel fetching (using public web address instead of internal ip address)
2024-04-03 13:45:08 +02:00
04dc7fcd30 # Admin backends
* disables certain admin backends
* adds proper ordering to server message admin overview
2024-04-03 08:29:19 +02:00
09546212b9 # Admin button
* adds button for easier admin backend access
2024-04-03 08:26:00 +02:00
b1cd7dee40 # JSON Decode error catch
* adds error catching on wfs parcel resolving
2024-03-15 09:10:06 +01:00
c772e1de06 Merge remote-tracking branch 'origin/master' 2024-03-12 10:32:17 +01:00
4332a750d1 # Message rendering
* adds icons to message danger, info and success rendering
2024-03-12 10:32:05 +01:00
47279dd55d # Requirements
* updates requirements.txt
2024-03-11 08:12:21 +01:00
e2eb0ecbb0 HOTFIX
* fixes bug where rectangular geometry results in an error during geometry complexity calculation
2024-02-29 18:37:53 +01:00
be5b8457a6 HOTFIX
* downgrades package qrcode from 7.4.2 to 7.3.1. Further details can be found in https://github.com/lincolnloop/python-qrcode/issues/353
2024-02-22 18:18:24 +01:00
72f1d80261 # HOTFIX
* drops need for authentication for calculated parcels of an entry (reports are publicly available -> does not need auth!)
2024-02-21 18:31:43 +01:00
df55c16498 Merge pull request '# 382 - Redirect as 404' (#386) from 382_Custom_response_for_Validation_Error into master
Reviewed-on: SGD-Nord/konova#386
2024-02-16 10:16:05 +01:00
11cc8b6766 # 382 - Redirect as 404
* extends 404 template (user should check the URL)
* introduces new decorator "uuid_required" which performs a check on a given 'uuid' or 'id' parameter
    * throws a Http404 exception --> redirect to 404 template instead of 500 error template
2024-02-16 10:13:43 +01:00
0b5691f501 Merge pull request 'geom_parcel_improvements' (#384) from geom_parcel_improvements into master
Reviewed-on: SGD-Nord/konova#384
2024-02-16 08:44:38 +01:00
d76a1fc85f # Fixes
* drops unused methods
* fixes typos
* updates comments
* drops unused model attribute
2024-02-16 08:41:03 +01:00
476447c621 # CONN_MAX_AGE
* dropping conn_max_age due to problems with usage in gunicorn
2024-02-16 08:23:14 +01:00
2b94e537ae # Typo
* fixes typo
2024-02-16 08:14:42 +01:00
c06088a260 # Renaming
* renames a method and fixes doc string
2024-02-16 08:13:10 +01:00
4fc15f6a9d # Optimizations and fixes
* drops identifier handling on all edit-forms (identifier editing has been disabled on the frontend for a while now)
* updates test cases
* updates form caption for checking and recording action (less intimidating)
* optimizes district column width
* fixes bug on frontend parcel fetching on detail view
* adds extended tooltip for title column on index tables
* retraslates 'Law' to 'Rechtsgrundlage'
2024-02-08 07:31:19 +01:00
cf90f9710c # Geom parcel performance improvement
* refactors parcel calculating, resulting in 1.3-1.6x better performance
* optimizes parcel fetching view
2024-01-17 11:22:21 +01:00
8bcccb4685 # WIP: Performance boost parcel calculation
* improves handling of parcel calculation (speed up by ~30%)
* ToDo: Clean up code
2024-01-16 07:57:29 +01:00
50bd6feb89 # Issue #381
* adds another validity check to SimpleGeomForm (is_size_valid) to make sure the area of the entered geometry is somehow rational (>= 1m²)
* optimizes performance of django command sanitize_db
* extends Geometry model with two new attributes, holding timestamps when a parcel calculation has been started and ended
* finally drops unused update_parcel_wfs in favor of update_parcels in Geometry model
* refactors update_parcel method
* adds geometry buffer fallback in schneider/fetcher.py to avoid emptying of geometries when parcels shall be fetched
* finally removes utils/wfs/spatial.py
* extends GeomParcelsView according to #381
* updates translations
* removes redundant psycopg2-binary requirement
2024-01-09 13:11:04 +01:00
d911f4a3a3 Merge pull request 'django_5' (#378) from django_5 into master
Reviewed-on: SGD-Nord/konova#378
2024-01-05 09:37:18 +01:00
ab5cdfbcbf Django5
* updates requirements.txt
2024-01-05 09:34:18 +01:00
814afb39ae Netgis map client fix
* fixes performance issue on drawing when WFS is activated
2023-12-28 15:12:06 +01:00
c2ef5160a3 Django5
* updates requirements
* adds Django5 related migration and setting (DEFAULT_AUTO_FIELD)
2023-12-19 08:54:42 +01:00
e63fb6b8b6 Merge pull request 'EGON document fix' (#374) from 373_EGON_document into master
Reviewed-on: SGD-Nord/konova#374
2023-12-13 13:37:23 +01:00
425ebe54e6 EGON document fix
* fixes bug where documents have not been serialized properly into a single xml list
2023-12-13 13:34:46 +01:00
a26d349ad2 Merge pull request '# Django 4 migrations' (#371) from django_4_migrations into master
Reviewed-on: SGD-Nord/konova#371
2023-12-12 07:34:40 +01:00
a127b4f68c Merge pull request '368_Mail_linking' (#370) from 368_Mail_linking into master
Reviewed-on: SGD-Nord/konova#370
2023-12-11 13:42:51 +01:00
7b3b40f3c9 Mail links
* adds direct object links into mail templates
* refactors transferring app-model identification data from fore- to background (celery) properly
2023-12-11 13:40:32 +01:00
b1e7acc5f4 Municipal calculation to background
* moves municipal names fetching from fore- to background for mail sending
2023-12-11 12:12:19 +01:00
743bb320d7 Object mailing restructred
* restructures object info mail sending
2023-12-11 12:06:33 +01:00
0a6918942a Merge pull request 'Custom exception reporter' (#369) from custom_exception_reporter into master
Reviewed-on: SGD-Nord/konova#369
2023-12-11 11:22:12 +01:00
15029cb93f Custom exception reporter
* adds custom exception_reporter.py
2023-12-11 09:40:17 +01:00
75ab281799 HOTFIX netgis client
* fixes bug where imported geometries would not be editable
2023-12-07 06:43:11 +01:00
4fd69b3249 Hotfix map client edit errors 2023-12-05 07:28:57 +01:00
aedcb7228e # Hotfix netgis client
* adds import functionality as it was
* fixes bug where z-index values have been too high
2023-12-05 06:54:55 +01:00
80f4118ee3 # Django 4 migrations
* migrations created because of switch to Django4
2023-11-30 13:04:33 +01:00
a7b84f31a8 Merge pull request 'django_4' (#363) from django_4 into master
Reviewed-on: SGD-Nord/konova#363
2023-11-30 12:40:54 +01:00
4000eed75d Merge pull request 'Hotfix: EcoAccount serializable' (#361) from 360_EcoAccount_not_serializable into django_4
Reviewed-on: SGD-Nord/konova#361
2023-11-29 12:22:42 +01:00
07b14f1b7f Hotfix: EcoAccount serializable
* fixes bug where EcoAccount model was not serializable due to changes in newer DAL version due to Django4
2023-11-29 12:21:37 +01:00
8132427c7c # CSRF_TRUSTED_ORIGINS
* new in Django4: setting CSRF_TRUSTED_ORIGINS needs to be set to schema+host for new CSRF security handling
2023-11-28 12:59:40 +01:00
401dc18731 HOTFIX
* fixes z-index overlapping of netgis map client toolbar and modal forms
2023-11-27 12:51:39 +01:00
a2389edcc1 Fix for testing 2023-11-21 14:03:38 +01:00
92797f4ea0 Merge pull request 'master' (#357) from master into django_4
Reviewed-on: SGD-Nord/konova#357
2023-11-21 13:38:18 +01:00
d5e273ee80 Merge pull request 'django_4_map_update' (#356) from django_4_map_update into django_4
Reviewed-on: SGD-Nord/konova#356
2023-11-21 13:35:35 +01:00
34733779ed Map update 2023-11-21 13:22:58 +01:00
b1ee0f9034 Map update 2023-11-21 12:53:27 +01:00
a4ec0a8722 Map update 2023-11-21 09:01:59 +01:00
2aaa39e3a6 HOTFIX
* fixes bug on saving eco account with missing deductable surface value
2023-11-07 16:23:57 +01:00
964a9aaed9 # Netgis HOTFIX
* fixes bug where map client would not parse config.json properly resulting in failed client start
2023-10-30 09:40:35 +01:00
17de730c53 HOTFIX
* corrects behaviour of lanis link generation for EIV and KOM
2023-10-26 07:26:38 +02:00
23c7a80bd7 HOTFIX
* fixes bug where detail view of KOM could not be opened anymore
2023-10-26 07:22:09 +02:00
3e593bd00e Merge pull request '# 349 LANIS link improvements' (#350) from 349_LANIS_link into master
Reviewed-on: SGD-Nord/konova#350
2023-10-25 10:09:39 +02:00
16fd7a3a2c # 349 LANIS link improvements
* replaces 'dumb' link template with LANIS mapinterface support
* adds fallback default LANIS link
2023-10-25 10:04:56 +02:00
235539ee4b # Map client update
* updates netgis map client to newest pre-release
2023-10-16 13:27:27 +02:00
157e733c5a Django 4.2
* updates Django to 4.x and other packages (if possible) to latest versions
* Attention: Requires postgresql >= 12.0
* updates code fragments to match requirements of newer package versions
2023-10-12 09:57:05 +02:00
bfa893a02f Merge pull request 'test' (#347) from test into master
Reviewed-on: SGD-Nord/konova#347
2023-09-15 13:12:40 +02:00
e63de9b628 # Bugfix Payment date invalid
* fixes a bug where validation check of payment 'due' has not been triggered properly
2023-09-13 14:40:22 +02:00
9117abd1d8 Unit test user app
* adds unit test for User model and forms
* refactors functions from user_checks.py into User class and drops user_checks.py
2023-09-13 09:49:40 +02:00
19baf7ba86 # Unit test user
* adds unit test for team creating and editing of user app
2023-09-12 11:49:12 +02:00
d69ea6d7c0 # Unit tests for konova geometry
* adds further unit tests for konova app geometry model
2023-09-12 09:16:10 +02:00
89efc33d75 # Unit test konova app
* adds unit test for konova app models
* drops unused/unnecessary code fragments
* updates translation
2023-09-08 12:47:50 +02:00
530ceb3876 Unit test konova base forms
* adds unit test for resubmission modal form
2023-09-07 11:07:17 +02:00
ed548736e0 Unit test intervention/konova
* adds unit test for intervention app
* adds unit test for konova app
2023-09-07 10:48:11 +02:00
366c3eec83 Unit tests ema
* adds unit tests for ema forms
2023-09-05 11:24:29 +02:00
6362fbc387 Unit tests intervention
* adds tests for share and revocation forms
2023-08-31 12:17:28 +02:00
4a70408ec3 Deduction validity checking
* fixes behaviour of related deduction checks on intervention checking
2023-08-31 11:31:33 +02:00
5d734638ab Eco account unit tests
* adds eco account unit tests
* adds validity check to eco account form to check on existing deductions and potential conflict with reduced deductable surface
* improves geojson handling on SimpleGeomForm
* adds/updates translation
2023-08-30 16:20:06 +02:00
4392401f27 Unit test compensation models
* adds unit tests for compensation models
* removes duplicated unit tests
2023-08-30 10:37:16 +02:00
777b7a929d Merge branch 'master' into test
# Conflicts:
#	locale/de/LC_MESSAGES/django.mo
#	locale/de/LC_MESSAGES/django.po
2023-08-30 09:12:02 +02:00
454805608c Merge pull request '345_Other_deadline_with_comment' (#346) from 345_Other_deadline_with_comment into master
Reviewed-on: SGD-Nord/konova#346
2023-08-29 14:07:04 +02:00
13da5dbc32 # Deadline form logic
* adds logic to NewDeadlineModalForm to invalidate 'other' deadline types without comment (as explanation for 'other')
2023-08-29 14:06:11 +02:00
0a3b91e69a #345 Fix
* adds is_valid check for NewDeadlineModalForm to implement #345
2023-08-29 10:55:03 +02:00
be2245dbd9 Merge pull request '# 342 Fix' (#343) from 342_Rounding_error_on_db_SUM into master
Reviewed-on: SGD-Nord/konova#343
2023-08-25 09:41:55 +02:00
1c50d66551 # 342 Fix
* fixes bug where rounding error on aggregated db SUM() would occur
* simplifies code base
2023-08-25 09:13:46 +02:00
7430a239a5 Unit test intervention forms
* adds unit test for new/edit intervention forms
* improves code base for generating new identifiers
2023-08-24 11:47:40 +02:00
02e8d65f02 Unit test EMA model
* adds unit test for EMA model
2023-08-24 10:59:32 +02:00
f1f73d0a66 Unit test compensation states
* adds unit test for adding/editing/removing compensation states
2023-08-22 10:54:20 +02:00
147d4938db Unit test for compensation forms
* adds unit tests for adding and editing deadline
2023-08-21 10:33:05 +02:00
b802c02069 Unit test for compensation forms
* adds compensation action forms unit tests
2023-08-21 10:10:23 +02:00
1047a5f119 Tests on analysis and compensation
* enhances tests for analysis and compensation app
2023-08-17 12:59:50 +02:00
b854695399 Unit test api
* adds unit test for APIUserToken
* enhances handling of token fetching for API
2023-08-17 10:44:58 +02:00
1726eb38ad Unit test analysis
* adds unit test for creating report
* fixes bug where new (>2018) eco accounts have not been fetched correctly from the  db
* adds enhancements in the frontend
* improves test data setup
2023-08-17 10:12:05 +02:00
865a3a51fe Class based views
* refactors method based views for parcel fetching, home and logout to class based
2023-08-15 11:29:38 +02:00
6de3fab800 Merge pull request 'Error on map client search' (#340) from 338_Error_on_map_client_search into master
Reviewed-on: SGD-Nord/konova#340
2023-07-10 11:47:12 +02:00
c8b9f28584 Error on map client search
* adds info for user if address search content could not be parsed properly due to external errors
2023-07-10 10:23:04 +02:00
46d40205f2 Merge pull request 'Geometry simplification' (#339) from 337_Simplify_large_geometries into master
Reviewed-on: SGD-Nord/konova#339
2023-07-10 10:07:49 +02:00
e8feec851f Geometry simplification
* simplifies geometries on SimpleGeomForm if threshold GEOM_MAX_VERTICES has been exceeded
    * geometry is iteratively simplified to find a proper tolerance value which satisfies the GEOM_MAX_VERTICES threshold
2023-06-28 14:21:26 +02:00
dd9d10f6fc Merge pull request '# Improves form date checking' (#335) from 334_nrealistic_dates into master
Reviewed-on: SGD-Nord/konova#335
2023-05-17 14:39:50 +02:00
9136b89e00 # Improves form date checking
* adds validator to make sure no dates like `01.01.1` can be accepted. All dates must be somewhat later than 01.01.1950
2023-05-17 14:08:57 +02:00
67e050764b Merge pull request '# Fixes bug' (#332) from 331_Deductions_of_unshared_interventions_not_changeable into master
Reviewed-on: SGD-Nord/konova#332
2023-05-16 14:10:59 +02:00
0664604804 # Fixes bug
* fixes bug described in #331
2023-05-16 12:11:13 +02:00
dc052a96cb Merge pull request '#328 Fix' (#329) from 328_Removing_of_migrated_revocations_fails into master
Reviewed-on: SGD-Nord/konova#329
2023-04-26 11:29:43 +02:00
872d2bd9f8 #328 Fix
* fixes bug described in #328
2023-04-26 11:28:46 +02:00
e05aca7fa7 Merge pull request '#325 Fix' (#326) from 325_Error_on_ecoaccount_recording into master
Reviewed-on: SGD-Nord/konova#326
2023-04-19 15:23:15 +02:00
eaceb2d6fe #325 Fix
* fixes bug described in #325
2023-04-19 15:22:52 +02:00
7bbba6f7d3 Merge pull request '# Implements #332' (#323) from 332_Drop_deductions_of_deleted_intervention into master
Reviewed-on: SGD-Nord/konova#323
2023-03-30 15:11:43 +02:00
8e89beaf88 Simplification
* simplifies fetching of intervention's deductions
2023-03-30 15:11:19 +02:00
24a9a7d695 # Implements #332
* extends intervention's mark_as_deleted() functionality to drop related deductions and free reserved deductable surface from the related eco accounts
2023-03-30 15:08:42 +02:00
b655b47979 Merge pull request 'Adds scale line to map client' (#320) from scale_line_map_client into master
Reviewed-on: SGD-Nord/konova#320
2023-03-28 13:52:32 +02:00
3d967341e1 Adds scale line to map client 2023-03-28 13:51:28 +02:00
6aa243192d Merge pull request 'Server Messages unpublish' (#319) from improve_unpublish_dependency_news into master
Reviewed-on: SGD-Nord/konova#319
2023-03-24 07:35:57 +01:00
7fdc93fefd Server Messages unpublish
* changes unpublish_on to optional value
* simplifies fetching of server message news
2023-03-24 07:34:50 +01:00
91537078eb Merge pull request 'HOTFIX' (#317) from fix_public_report into master
Reviewed-on: SGD-Nord/konova#317
2023-03-24 07:14:13 +01:00
b5937b516e HOTFIX
* fixes bug where float numbers could not be used as input for e.g. buffer radius
    * supports
2023-03-24 07:13:15 +01:00
431b400ca8 Merge pull request '#314 Public report for old entries' (#315) from 314_Public_report_on_old_entries into master
Reviewed-on: SGD-Nord/konova#315
2023-03-22 09:00:01 +01:00
d5f97687b5 #314 Public report for old entries
* enables public access to reports for unrecorded old entries if their binding_date < 16.06.2018
2023-03-22 08:54:23 +01:00
b34be9f2fd Merge pull request 'netgis_client_update' (#312) from netgis_client_update into master
Reviewed-on: SGD-Nord/konova#312
2023-03-16 08:14:16 +01:00
e2cbe7eb73 HOTFIX
* fixes bug where float numbers could not be used as input for e.g. buffer radius
    * supports up to two digits
2023-03-16 08:12:51 +01:00
0fb40a5568 Netgis client update
* adds bugfixes and improvements
    - point / line auto buffer key input change buffer
    - pass default buffer values from config
    - update area label while vertex editing
    - auto buffer remove source geoms when done
    - toggle cut tool off when done
    - toggle delete tool off when done
    - allow panning while vertex editing (middle mouse button)
2023-03-13 08:06:24 +01:00
78d0bb876f Merge pull request '# 308 To share info message' (#310) from 308_To_share_info_message into master
Reviewed-on: SGD-Nord/konova#310
2023-03-13 06:55:46 +01:00
d5523e6fb6 Merge pull request 'POST form error fix' (#309) from increase_data_upload_memory_size into master
Reviewed-on: SGD-Nord/konova#309
2023-03-13 06:55:27 +01:00
ea9083f4c5 POST form error fix
* increases threshold for max upload memory size from 2.5MB to 5MB
2023-03-13 06:54:32 +01:00
f22b45b82b # 308 To share info message
* adds needs-to-be-shared info message on entries which are only shared with the current user
2023-03-07 07:17:08 +01:00
4d3831f30b Merge pull request '# Bugfix' (#306) from fix_multipolygon_features_mapclient into master
Reviewed-on: SGD-Nord/konova#306
2023-02-23 15:34:34 +01:00
b494bee65e # Bugfix
* fixes bug where multipolygon behaved in mapclient as single polygon, making e.g. deleting of single polygons impossible without removing everything
2023-02-23 14:56:49 +01:00
fb2900aa74 HOTFIX
* fixes bug where quality checker for compensations would not check properly for state surface sums
2023-02-23 12:02:50 +01:00
d600ab1d59 HOTFIX
* fixes bug where quality checker for compensations would not check properly for state surface sums
2023-02-23 10:44:44 +01:00
35745d6ee6 Merge pull request '284_285_API_changes' (#296) from 284_285_API_changes into master
Reviewed-on: SGD-Nord/konova#296
2023-02-23 10:20:56 +01:00
dfa05a98c6 Merge pull request '299_Performance_tweaks' (#302) from 299_Performance_tweaks into master
Reviewed-on: SGD-Nord/konova#302
2023-02-23 10:19:55 +01:00
f86952be06 Merge pull request '#300 Extend mail templates' (#301) from 300_Extend_mail_template into master
Reviewed-on: SGD-Nord/konova#301
2023-02-23 10:18:32 +01:00
bf41559c56 #300 Extend mail templates
* extends all relevant mail templates such that municipals of an entry will be shown in the mail
2023-02-23 10:17:45 +01:00
8fccddf66f # Reduces db access
* reduces number of queries performed on detail views of intervention, compensation and eco_account
* renders deductable_rest of eco account beneath progressbar on eco account index view
    * clarifies ordering logic of related column
2023-02-22 10:53:25 +01:00
cea40cd878 # Improves home_view()
* improves db fetching performance of landing page by ~75%
2023-02-22 10:02:56 +01:00
799b97341a # Improves filter_show_all()
* improves performance for filter_show_all() in ShareableTableFilterMixin and CheckboxCompensationTableFilter by ~40%
2023-02-22 09:44:35 +01:00
6653269427 # Improve is_shared_with()
* improves central is_shared_with() method of ShareableObjectMixin to run ~30% faster
2023-02-22 09:19:22 +01:00
e1259b276c Merge pull request 'recorded_quality_check' (#297) from recorded_quality_check into master
Reviewed-on: SGD-Nord/konova#297
2023-02-21 08:06:36 +01:00
a9b29409d3 Deployment preparation
* adds unrecording to invalid entries
* reduces quality check runs on entries of interest (compensations)
2023-02-21 08:03:56 +01:00
80c4bd5441 Specific quality check for recorded entries
* adds a new command specifically for recorded entries
2023-02-17 08:03:10 +01:00
9b24fcdaf2 Merge pull request 'netgis_client_update' (#294) from netgis_client_update into master
Reviewed-on: SGD-Nord/konova#294
2023-02-13 14:41:06 +01:00
728402b4e1 # Disables buggy client functions
* disables auto-buffering of new points and lines
* improves rendering of surface size
2023-02-13 12:00:19 +01:00
ddbf6d570e # Zoom level for parcel wfs 2023-02-13 11:24:24 +01:00
8bceebd71e Merge branch 'map_client_parcel_wfs_proxy' into netgis_client_update 2023-02-13 11:17:45 +01:00
176b8fe504 # Server proxy for client parcel wfs
* refactors map_proxy.py
* adds proxy support for parcel wfs
2023-02-13 10:55:58 +01:00
dac060e62d # Server proxy for client parcel wfs
* refactors map_proxy.py
* adds proxy support for parcel wfs
2023-02-13 09:58:56 +01:00
71977192b8 Netgis Client update 2023-02-13 09:03:09 +01:00
bb72417bf6 Merge pull request '#290 Egon exporter file name' (#291) from 290_EGON_Exporter_file_name into master
Reviewed-on: SGD-Nord/konova#291
2023-02-06 15:01:29 +01:00
c0ff113ff2 #290 Egon exporter file name
* replace user given file name with file based file name for egon export handling
2023-02-06 15:00:34 +01:00
6add878e22 Merge pull request 'Bugfix' (#289) from fix_shared_users_on_comps into master
Reviewed-on: SGD-Nord/konova#289
2023-02-02 16:34:47 +01:00
676a76acf3 Bugfix
* fixes rendering of shared users counter on unshared compensation entries
2023-02-02 16:34:09 +01:00
93cc17b01a Quality Check Command enhancement
* adds fix for dealing with __proxy__ instances
2023-02-01 14:17:05 +01:00
a578821fd9 Merge pull request 'Quality Check Command' (#286) from command_quality_check into master
Reviewed-on: SGD-Nord/konova#286
2023-02-01 14:09:15 +01:00
bdbe2e91ce Quality Check Command
* adds new command 'quality_check' which performs the quality checker on certain entries, which can be filtered using '--identifier-like' and/or '--title-like' parameters
    * results are shown in terminal
2023-02-01 14:08:39 +01:00
882468cde6 #285 Drop atom_id from API
* refactors code usage from atom_id to id inside of api app
2023-02-01 08:08:52 +01:00
123d02abf9 #284 Empty API data
* adds proper message for certain data parsing in case of an error
2023-02-01 07:03:54 +01:00
d91e9d016c # WIP: Netgis Client Update 2023-01-31 16:26:59 +01:00
794001a8ae Merge pull request 'Empty value egon fix' (#282) from empty_egon_fix into master
Reviewed-on: SGD-Nord/konova#282
2022-12-22 07:55:38 +01:00
2d17b9cc65 Merge pull request '#280 Schneider capability' (#281) from 280_Schneider_capability into master
Reviewed-on: SGD-Nord/konova#281
2022-12-22 07:42:39 +01:00
acb022ea13 Empty value egon fix
* adds support for missing values so that EGON can properly handle these entries
2022-12-22 07:25:55 +01:00
5e48202226 HOTFIX: Migrated revocation
* adds handling for error raising if migrated revocation document missing, due to no existing document at all
2022-12-14 16:36:21 +01:00
c7aa90aa5b #280 Schneider capability
* refactors update_parcels() method in Geometry model to work on Schneider
* old WFS based logic still exists as update_parcels_wfs() in Geometry model to have a fallback. Can be deleted in the future
2022-12-14 12:18:18 +01:00
8f89217ac1 Merge pull request '#277 Deleted entries accessible' (#278) from 277_Deleted_entries_accessible into master
Reviewed-on: SGD-Nord/konova#278
2022-12-13 09:16:16 +01:00
3a6111d2ec #277 Deleted entries accessible
* fixes bug where deleted entries could be accessed if detail page would be called directly
2022-12-13 09:15:22 +01:00
1a71f64db7 Merge pull request 'EGON Export fixes' (#275) from egon_export_fixes into master
Reviewed-on: SGD-Nord/konova#275
2022-12-13 06:50:09 +01:00
0d58ed0501 EGON Export fixes
* replaces missing value 'None' with empty string ''
2022-12-13 06:49:03 +01:00
dd33085e3c Merge pull request 'Public report enhancements' (#274) from enhancements_report_template into master
Reviewed-on: SGD-Nord/konova#274
2022-12-12 13:12:31 +01:00
eda1c7a532 Public report enhancements
* adds toggling of scrollable box table views
* deactivates scrolling for public report view (so all entries can be seen if page is printed)
2022-12-12 13:09:17 +01:00
b5bf63798d Merge pull request '#271 Identifier non editable' (#272) from 271_Identifier_non-editable into master
Reviewed-on: SGD-Nord/konova#272
2022-12-09 13:02:11 +01:00
a5cc86798c #271 Identifier non editable
* sets the identifier form field as readonly
* extends help text
* updates translations
2022-12-09 12:43:49 +01:00
0de60c00e8 Merge pull request 'configurable_label_input_ratio' (#269) from configurable_label_input_ratio into master
Reviewed-on: SGD-Nord/konova#269
2022-12-08 10:16:23 +01:00
63d37183de #268 Filter multiple parcelgroups
* adds filter support for multiple parcelgroup and district names, separated by ','
2022-12-08 10:14:39 +01:00
0f6867605e Template enhancements
* adds configurable label-input ratio setting for forms and specializes for RemoveModalForm
* enhances form body html structure for better UX and usage of label-input ratio
2022-12-08 09:48:01 +01:00
e85065d43b Merge pull request 'Minor_issues' (#267) from Minor_issues into master
Reviewed-on: SGD-Nord/konova#267
2022-12-07 07:04:48 +01:00
6162d41df3 #255 Filter by created user
* adds new checkbox filter for all major data types wich shows only entries, where the performing user has been the initial creator of
* adds help texts for checkbox filters
* adds translations
2022-12-06 08:30:43 +01:00
f25e85493d #262 Public report missing entry placeholder
* adds empty value rendering on public intervention report
2022-12-06 07:22:11 +01:00
590e0c6288 #257 Missing geometry message red
* changes message colour from blue to red (indicating 'blocking' message)
* only renders message on read_only form e.g. on detail view
2022-12-06 07:19:39 +01:00
a7897f7910 Hotfix: Resubmission mail
* fixes resubmission mail recipient
2022-12-05 06:55:35 +01:00
734fa32f38 Merge pull request 'egon_sending_on_edit' (#263) from egon_sending_on_edit into master
Reviewed-on: SGD-Nord/konova#263
2022-12-05 06:09:59 +01:00
edcf266dfa Egon sending
* adds sending to EGON (again) when Intervention is recorded
2022-12-05 06:06:52 +01:00
cf7cedaa34 Fixing broken document migration
* adds changes to document migration to correctly migrate documents
2022-12-02 12:57:18 +01:00
3574b315eb Merge pull request 'EGON GML Payment date' (#260) from fix_egon_payment_date into master
Reviewed-on: SGD-Nord/konova#260
2022-12-02 06:43:05 +01:00
8d96ede2b4 EGON GML Payment date
* fixes bug where missing payment date would result in no egon message sent
2022-12-01 15:35:45 +01:00
33af4ddf2b Merge pull request 'Fixes resubmission handling' (#258) from resubmission_fix into master
Reviewed-on: SGD-Nord/konova#258
2022-12-01 14:01:08 +01:00
8e104b7efc Fixes resubmission handling
* resubmissions have not been deleted after sending mails
2022-12-01 13:57:04 +01:00
b24e461e06 Hotfix
* fixes bug where None-geometry entry (instead of empty geometry) would not be expected on parcel fetching
2022-11-30 07:06:44 +01:00
029b9cf794 Revert "File number public reports"
This reverts commit 90fffb9576.
2022-11-28 13:50:31 +01:00
059972b4cd Merge pull request 'File number public reports' (#252) from remove_file_numbers_public_report into master
Reviewed-on: SGD-Nord/konova#252
2022-11-28 07:28:52 +01:00
90fffb9576 File number public reports
* removes file numbers from public reports
2022-11-28 07:28:09 +01:00
155d9d1d38 Merge pull request '222_Annual_reports' (#251) from 222_Annual_reports into master
Reviewed-on: SGD-Nord/konova#251
2022-11-25 09:18:04 +01:00
b8d9343682 Command response dynamic
* adds a check whether the mail could be sent properly or not and changes the resulting response
2022-11-25 09:17:15 +01:00
4ab713a908 Merge pull request '249_Last_modified_created' (#250) from 249_Last_modified_created into master
Reviewed-on: SGD-Nord/konova#250
2022-11-25 09:05:48 +01:00
f2c5e7ae01 Tests
* extends test for new behaviour of newly created entries
2022-11-25 09:05:06 +01:00
e048d44c95 #249 Created as modified
* fills modified attribute on new entries with created value automatically
* adds default ordering by last modified on table overviews
2022-11-25 08:27:42 +01:00
ad02a62eaf Merge pull request 'hotfix_geometry_save_race_condition' (#246) from hotfix_geometry_save_race_condition into master
Reviewed-on: SGD-Nord/konova#246
2022-11-23 16:06:50 +01:00
49d02b31f5 API - Geometry empty
* removes mapping of empty geometry to None due to general switch to empty geometry usage
2022-11-23 16:05:27 +01:00
0b2cf2a0a4 Geometry race condition fix
* fixes race condition for geometry conflict and parcel calculation
* harmonizes empty geometries from None/MultiPolygonEmpty to MultiPolygonEmpty
2022-11-23 13:51:05 +01:00
618cf3a756 Merge pull request '243_GDALException_on_null' (#244) from 243_GDALException_on_null into master
Reviewed-on: SGD-Nord/konova#244
2022-11-23 07:13:17 +01:00
e06e0e8306 Further fixes
* fixes race condition on geometry conflict calculation if performed in background process
* simplifies access to smaller buffered geometry
* adds mapping of "qm"->"m2" for UnitChoice in API usage for backwards compatibility
2022-11-22 15:38:03 +01:00
870d822c3a #243 Feature without geometry
* fixes GDALException in case of provided feature (import) without geometry content
* modifies 500.html template to inform the user about the admins being informed automatically
2022-11-22 14:49:51 +01:00
cf874225c1 Hotfix
* adds missing migration
2022-11-18 16:22:24 +01:00
25cd104cf6 Merge pull request 'Updates LANIS link' (#240) from lanis_link_fix into master
Reviewed-on: SGD-Nord/konova#240
2022-11-18 13:32:12 +01:00
ec0a5fefbd Merge pull request '#238 Fix' (#239) from 238_Type_error_on_eco_account_edit into master
Reviewed-on: SGD-Nord/konova#239
2022-11-18 13:29:11 +01:00
c50632e7a0 Updates LANIS link
* changes LANIS link to new layer declaration
2022-11-18 13:28:13 +01:00
3cd23b1761 #238 Fix
* adds casting from Decimal() to primitive float for proper calculation
2022-11-18 13:24:36 +01:00
e664fbbb7d Merge pull request 'Egon Payment compatibility' (#237) from fix_egon_payment_sum into master
Reviewed-on: SGD-Nord/konova#237
2022-11-18 07:54:58 +01:00
feed774679 Egon Payment compatibility
* EGON expects the payment amount to be a localized string instead of float
    * adds transformation for this
2022-11-18 07:40:35 +01:00
746ee7a283 Merge pull request 'fix_comp_action_units' (#235) from fix_comp_action_units into master
Reviewed-on: SGD-Nord/konova#235
2022-11-18 06:52:59 +01:00
b0f9ee4ac0 Z-axis geometry upload fix
* adds clamping of 3D geometries to 2D geometries if uploaded using the map importer
* extends tests for payment-document linkage
* fixes bug in team-admin selection where autocomplete could not be resolved properly
2022-11-17 13:01:40 +01:00
814f35c426 Fix CompensationAction unit None
* adds correct declaration of unit (qm -> m2) for template rendering
* adds migration to transform existing qm units to m2
2022-11-17 10:13:22 +01:00
7c4940729c Merge pull request 'post_release_fixes' (#233) from post_release_fixes into master
Reviewed-on: SGD-Nord/konova#233
2022-11-17 06:54:42 +01:00
7d0c405f58 #232 Payment document linkage
* adds error message on intervention view if a payment has been added but no document has been uploaded yet
* adds same check to quality checker, meaning no intervention can be recorded which has a payment but no document
* adds trigger for sending data to egon on uploading a document in case of an already existing payment
* adds translations
2022-11-16 16:11:42 +01:00
cbb137a902 #230 Shared users not rendered on compensation
* fixes non rendering of shared users on shared data
2022-11-16 13:30:07 +01:00
14fee4474f #229 Shared users mandatory on admin
* changes mandatory state of users and teams on admin backend to optional (as expected by the model)
* adds team selection to admin backend
2022-11-16 13:27:57 +01:00
deb97fbbf3 #228 Parcel filter
* changes parcel related filter fields from CharField to NumberField to avoid unexpected behaviour on non-numerical input
2022-11-16 13:18:52 +01:00
eb2d01eeea #231 Report geometry
* should fix report geometry cast problem in cases where MultiPolygon(srid=4326) is taken as default
2022-11-16 12:20:16 +01:00
933332c1ef Merge pull request 'Team mail fix' (#226) from 225_Team_mails_always_sent into master
Reviewed-on: SGD-Nord/konova#226
2022-11-14 07:23:15 +01:00
11e5d82086 Team mail fix
* filters team members by notification settings before sending team mails
2022-11-14 07:19:04 +01:00
7650e0bf73 Command
* adds new 'generate_report' command
   * generates TimeSpanReports for given conservation offices
   * zips reports into archive
   * sents archive to ADMINS mails
2022-10-26 15:44:33 +02:00
b1fe9ed9cb Bugfix
* fixes bug in excel report creation
* fixes order in laws of generated excel sheet
2022-10-26 10:35:15 +02:00
ddb1e82fbc Merge pull request 'minor_order_by_timestamp_improvement' (#223) from minor_order_by_timestamp_improvement into master
Reviewed-on: SGD-Nord/konova#223
2022-10-19 07:34:29 +02:00
1a8034fa20 Landing page shared count fix
* fixes bug where count of shared entries on landing page would ignore team-shared entries
* restore prior editable column icon rendering
2022-10-19 07:24:00 +02:00
a203d73471 Missing geometry html improvement
* improves rendering of missing geometry icons on table
2022-10-14 08:07:33 +02:00
e6c0d8b1cf Map client holes fix
* fixes bug where holes in stored geometries would not be rendered properly on initial loading the map client
    * drawback: multiple polygons are treated as a single feature on the map client. Not a real issue but maybe we can find a better solution to this
* quality of life: renders geometry area in m² on detail and report view
2022-10-14 08:02:08 +02:00
20c9950b7f Table improvements
* enhances visualization of editable column on tables
    * simplifies code
* enhances visualization of parcel_group column on tables
* WIP: Ordering on intervention table is odd. Same results are being displayed on page 2. Needs further analysis and fixing!
2022-10-12 16:26:01 +02:00
729a8f991c New help page link
* changes the help link to match the new starter help page
2022-10-12 10:20:04 +02:00
f4a1dd17b1 Map settings button removed
* removes map settings button, since there is no implementation and it is unclear, whether this will ever become a real feature
2022-10-12 10:18:57 +02:00
8e73387032 New icon for missing geometry
* introduces "..." as new icon for missing geometry entries
    * moves former explanatory message into title attribute for mouse hover activation
2022-10-12 10:15:27 +02:00
97b7156b9c Quality of Life improvements
* disables ordering of parcel_group ("Gemarkung") column on tables
    * ordering can not be done properly due to more complex nature of this column's content
* introduces "Keine Geometrie vorhanden" message instead of hour glass icon on entries where no geometry has been entered yet
* properly orders last_modified column by moving null values to the lower end of the ordering
2022-10-12 10:01:23 +02:00
18242d2cba Merge pull request '217_Prevent_recorded_deduction_deletion' (#220) from 217_Prevent_recorded_deduction_deletion into master
Reviewed-on: SGD-Nord/konova#220
2022-10-12 09:01:49 +02:00
2c20069dea Test extension
* adds further checks on EcoAccount tests for proper updating of new deductable_rest attribute
2022-10-12 08:59:38 +02:00
87b01e8fdd Fixes account deletion with deductions
* adds a warning on removing an eco account if there are still deductions
    * this way a user needs to get rid of these deductions first
2022-10-11 16:47:16 +02:00
bce271ceaa Fix for recorded deduction
* fixes bug where deduction of a recorded intervention could be deleted from the eco account detail view
* improves check_for_recorded_instance() logic
* improves rendering of detail view on compensation-like objects to highlight missing data
2022-10-11 16:32:12 +02:00
ac443bd9eb Merge pull request 'fix_acc_sorting_availability' (#218) from fix_acc_sorting_availability into master
Reviewed-on: SGD-Nord/konova#218
2022-10-11 15:21:23 +02:00
eb248be6f4 Fixes EcoAccount availability ordering
* adds db based table ordering for EcoAccountTable
2022-10-11 15:20:11 +02:00
ba7ae0b0b2 Hotfix for ordering of availability
* fixes error 500 in case of ordering by availability
2022-10-11 13:31:00 +02:00
27c7802760 Merge pull request 'Map client update' (#216) from map_tst into master
Reviewed-on: SGD-Nord/konova#216
2022-10-07 13:57:45 +02:00
6c8227ed17 Map client update
* updates map client to most recent code
* fixes bug on as_feature_collection which led to problems on initial loading in map
2022-10-07 13:56:43 +02:00
757598970b Merge pull request 'Netgis client update' (#214) from map_client_update_05102022 into master
Reviewed-on: SGD-Nord/konova#214
2022-10-05 11:01:55 +02:00
f38de97cf4 Netgis client update
* fixes address search results placement in scrollable context
* fixes import issues for gml and geojson
* updates basic configuration for map layers
2022-10-05 11:01:11 +02:00
750afdff08 Merge pull request '#208 API non existing atomID' (#212) from 208_API_non_existing_values into master
Reviewed-on: SGD-Nord/konova#212
2022-09-29 10:45:44 +02:00
b4cc919b02 #208 API non existing atomID
* handles error response for non existing atomID properly according to isse #208
2022-09-29 10:42:32 +02:00
49f57a4bf6 Merge pull request 'Fix geometry conflict message' (#211) from hotfix_geom_conflict_msg_with_deleted_entry into master
Reviewed-on: SGD-Nord/konova#211
2022-09-29 10:30:06 +02:00
c5f8e0c705 Fix geometry conflict message
* fixes bug where geometry conflicts template message has been rendered despite having no active geometry conflict
    * happened in case of existing geometry conflicts related to an as deleted marked entry
2022-09-29 10:29:33 +02:00
0bff9e0018 Merge pull request 'API Celery parcel calculation fix' (#209) from map_client_update into master
Reviewed-on: SGD-Nord/konova#209
2022-09-28 12:28:08 +02:00
0e9f0ba53e API Celery parcel calculation fix
* fixes bug where API stored data would not have parcels be calculated correctly
2022-09-28 12:24:06 +02:00
9ee0bddde9 Merge pull request 'Bugfix Parcel calculation' (#206) from map_client_update into master
Reviewed-on: SGD-Nord/konova#206
2022-09-16 12:13:24 +02:00
66a2387791 Bugfix Parcel calculation
* fixes a bug where neighbouring parcels would be detected using Intersection operation as well
2022-09-16 12:09:25 +02:00
d52db88a96 Merge pull request '202 Geometry conflict checking to background' (#204) from 202_Geometry_conflict_checking_to_background into master
Reviewed-on: SGD-Nord/konova#204
2022-09-15 12:13:17 +02:00
9bba982ce5 202 Geometry conflict checking to background
* wraps check_for_conflicts() in celery based method
* fixes bug on InterventionEditForm where geometry's save() has been called twice
2022-09-15 12:12:35 +02:00
0647116168 Merge pull request 'map_client_update' (#203) from map_client_update into master
Reviewed-on: SGD-Nord/konova#203
2022-09-15 11:39:00 +02:00
8591156aef Webfonts 2022-09-14 09:12:59 +02:00
b73b209344 Bugfix Webfonts Netgis Client 2022-09-14 09:10:20 +02:00
6a30af647a NEtgis Client Bugfix
* fixes bug where initial geometry has not been rendered on client loading
* needs to be replaced asap by a proper bugfix from the original devs
2022-09-13 13:43:43 +02:00
c08f5145fe Netgis client update
* adds new version to sources
2022-09-12 13:12:59 +02:00
968eea85d4 Merge pull request 'Login required on modals' (#201) from modal_login_required into master
Reviewed-on: SGD-Nord/konova#201
2022-09-12 12:11:15 +02:00
9283c12162 Login required on modals
* adds new login_required_modal decorator
    * can be used before regular login_required decorator to return a proper session-timed-out message
2022-08-25 11:34:09 +02:00
3f907c2b44 Merge pull request '104_Class_based_views' (#200) from 104_Class_based_views into master
Reviewed-on: SGD-Nord/konova#200
2022-08-22 11:09:51 +02:00
a4de394a54 Share views
* replaces function based share views with class based
* improves team-share autocomplete search
* renames internal share url names
2022-08-22 10:58:07 +02:00
a16f68012d EcoAccount views
* splits compensation/views/eco_account.py (+700 lines) into separate files in new module
    * view files can now be found in /compensation/views/eco_account/...
2022-08-22 10:17:49 +02:00
276e7ed4bb CompensationAction views
* replaces function based action views with class based
2022-08-22 08:38:23 +02:00
1b5cbba48b State views
* replaces function based state views with class based
2022-08-22 08:27:36 +02:00
d6057575ea Ema Deadline views
* replaces ema deadline views with class based
2022-08-22 08:12:55 +02:00
89c1352503 Deadline views
* replaces function based views for deadlines with class based views
2022-08-22 08:07:35 +02:00
ed82109af9 Document views
* replaces function based views for creating, editing, removing and fetching documents with class based views
    * implemented for all major data types
2022-08-22 07:52:22 +02:00
c80145366e Record class view
* adds AbstractRecordView to konova/views/record.py
    * implements for all major data types
2022-08-19 11:01:33 +02:00
096385a9eb Resubmission class view
* adds AbstractResubmissionView to konova app
    * implemented for all major data types
    * replaces function based views
2022-08-19 10:47:59 +02:00
7e68d12ee9 Konova views
* splits konova/views.py into separate files in new module
    * view files can now be found in /konova/views/...
* introduces first class based view AbstractLogView
    * implemented for Ema, Intervention, Compensation and EcoAccount
2022-08-19 10:25:27 +02:00
9654f6873d EcoAccount views
* splits compensation/views/eco_account.py (+700 lines) into separate files in new module
    * view files can now be found in /compensation/views/eco_account/...
2022-08-19 08:27:42 +02:00
e930250226 Compensation views
* splits compensation/views/compensation.py (+700 lines) into separate files in new module
    * view files can now be found in /compensation/views/compensation/...
2022-08-19 08:12:32 +02:00
b51f743be2 EMA views
* splits ema/views.py (+700 lines) into separate files in new module
    * view files can now be found in /ema/views/...
2022-08-19 07:51:49 +02:00
ac0db79928 Intervention views
* splits intervention/views.py (+700 lines) into separate files in new module
    * view files can now be found in /intervention/views/...
2022-08-19 07:34:09 +02:00
ad4d64457b Filters refactoring
* splits filters.py of compensation and ema app into separate files in new /filters module
* optimizes entry search for multi keyword input
2022-08-18 13:19:04 +02:00
d785285805 Autocomplete refactoring
* refactors konova/autocompletes.py by splitting into individual files and moving them to fitting apps
     * autocomplete files now live in APPNAME/autocomplete/...
2022-08-18 11:25:06 +02:00
4b3a35c30e Konova filter mixins
* refactors konova/mixins.py into individual files in konova/mixins/...
2022-08-18 10:45:15 +02:00
047a4c7a2e Tables refactoring
* splits compensation tables.py into individual files for compensation table and eco account table
2022-08-18 10:24:11 +02:00
0ce913c672 User forms
* refactors user/forms.py by splitting into modals package and regular forms
    * regular forms can now be found at user/forms/user.py and user/forms/team.py
    * modal forms can now be found at user/forms/modals/...
2022-08-18 10:17:09 +02:00
2f67c2f569 Intervention forms
* refactors intervention/forms and ../modalForms into individual files in separated packages
    * forms.py has been renamed into intervention.py, now can be found as intervention/forms/intervention.py
    * modalForms.py has been split into individual files living in modals package, can be found as intervention/forms/modals/...
2022-08-18 10:08:51 +02:00
ea7a53eb4f Compensation forms refactoring
* splits compensation/forms.py and /modalForms.py into individual files inside new packages
    * general forms stay in new files in compensation/forms
    * modal forms stay in new files in compensation/forms/modals
2022-08-18 09:54:49 +02:00
5550b8aa67 Merge pull request '197_Resubmission' (#198) from 197_Resubmission into master
Reviewed-on: SGD-Nord/konova#198
2022-08-15 11:02:07 +02:00
a6f7e605e6 Migrations + Cleanup
* adds needed migrations
* refactors forms.py (700+ lines) in main konova app
    * splits into forms/ and forms/modals and single class/topic-files for better maintainability and overview
* fixes bug in main konova app migration which could occur if a certain compensation migration did not run before
2022-08-15 10:50:01 +02:00
8bce8b8e75 Command
* adds new command to be used with cron for periodic checkin of resubmissions
* updates translations
2022-08-15 10:02:07 +02:00
4f02e8ee1b Templates + Routes
* adds control button for Intervention, Compensation, Ema and EcoAccount for setting a resubmission on an entry
2022-08-15 09:38:51 +02:00
117a4437fe Model
* adds new model and mixin
* adds new functionality for Mailer class for sending resubmission mails
2022-08-15 08:08:15 +02:00
f9c23a8f29 Merge pull request 'Fix' (#196) from 192_Deduction_modal_form_on_recorded_intervention into master
Reviewed-on: SGD-Nord/konova#196
2022-08-10 09:29:18 +02:00
a5f0d7f8c6 Fix
* adds more detailed situation check on check_for_recorded_instance()
2022-08-10 09:28:43 +02:00
982d9f1930 Merge pull request '191_Deduction_change_notification' (#195) from 191_Deduction_change_notification into master
Reviewed-on: SGD-Nord/konova#195
2022-08-10 09:04:29 +02:00
9840c8fa8f Minor changes
* updates translation
2022-08-10 09:03:24 +02:00
890911c2dc Mail sending
* adds mail sending logic for new notification setting
* adds new templates for user and team based sending
* enhances all email template layout
* adds translations
2022-08-10 08:59:24 +02:00
3d10e84852 New Notification
* adds new notification setting to user settings form
* adds translations
* adds initial creating of ENUM on setup command
2022-08-10 08:03:18 +02:00
05145673e1 Merge pull request '#190 Mandatory finished deadline' (#194) from 190_Deadlines_mandatory into master
Reviewed-on: SGD-Nord/konova#194
2022-08-08 15:01:31 +02:00
fd4c9b0e5e #190 Mandatory finished deadline
* adds template message to indicate a finished-deadline is mandatory
* adds finished deadline existance to quality check of compensation-like entries
* adds proper warning to quality check result
* extends tests
2022-08-08 14:57:36 +02:00
23f0567aef Merge pull request '#185 Parcel loading on public reports' (#186) from 185_Parcels_loading_CORS_error into master
Reviewed-on: SGD-Nord/konova#186
2022-08-02 09:38:44 +02:00
70555ee5a3 #185 Parcel loading on public reports
* fixes bug where unauthorized clients would not load a geometries parcel view properly
* minor general templates enhancements
2022-08-02 09:38:03 +02:00
1c38acea25 Revert "Merge branch 'Docker' into master"
This reverts commit cf282c937f, reversing
changes made to ee9834c0da.

Undos accidental remote merge
2022-07-13 11:21:51 +02:00
cf282c937f Merge branch 'Docker' into master 2022-07-13 11:02:57 +02:00
ee9834c0da Merge pull request 'geometry_srid_migration' (#183) from geometry_srid_migration into master
Reviewed-on: SGD-Nord/konova#183
2022-07-13 10:57:37 +02:00
dacfbd0504 Test update
* updates tests for SRID migration
2022-07-13 10:53:45 +02:00
0e7859e538 Geometry Model geom SRID migration
* adds two new migrations for transforming existing geometries into the default SRID
2022-07-13 08:19:27 +02:00
16107f93f6 Merge pull request '#180 Shared data users hidden' (#181) from 180_Visibility_of_contact_data into master
Reviewed-on: SGD-Nord/konova#181
2022-06-27 14:31:30 +02:00
d669adf54f #180 Shared data users hidden
* implements hidden visibility of shared users on non-shared entries
2022-06-27 14:31:09 +02:00
589a7aec60 Merge pull request '177_Minor_enhancements' (#178) from 177_Minor_enhancements into master
Reviewed-on: SGD-Nord/konova#178
2022-06-22 11:53:43 +02:00
3f3ae4e31b # 177 Impressum link
* adds impressum link to footer
2022-06-21 14:27:32 +02:00
1bbede0d12 # 177 Overview layout enhancement
* enhances layout title section layout on overview template
2022-06-21 14:25:50 +02:00
0801b3f6ab # 177 Timespanreport and Excel download
* fixes bug on excel download
* adds new order of columns to excel template for report download
* enhances subtitle for old data entries on timespanreport template
2022-06-21 14:19:10 +02:00
44ad156595 # 177 Timespanreport helptext for deductions
* adds help text for recorded deduction section
2022-06-21 14:11:01 +02:00
5c95bc7d85 # 177 Timespanreport column order
* rearranges the column order so 'Total' will always be the first column
2022-06-21 14:02:49 +02:00
3b045fea8e # 177 Report help texts
* adds report form field help texts
* adds translations
2022-06-21 13:54:41 +02:00
6e5237ab88 Merge pull request '# 175 Report law calculation bugfix' (#176) from 175_Report_law_calculation_wrong into master
Reviewed-on: SGD-Nord/konova#176
2022-06-15 17:46:49 +02:00
f19ad5f639 # 175 Report law calculation bugfix
* fixes bug where amount of used laws in intervention forms would not be calculated properly
2022-06-15 17:44:43 +02:00
766e3c8d37 Merge pull request 'master' (#174) from master into Docker
Reviewed-on: SGD-Nord/konova#174
2022-06-02 09:38:06 +02:00
e90625c8b5 Merge pull request 'master' (#172) from master into Docker
Reviewed-on: SGD-Nord/konova#172
2022-06-02 08:53:32 +02:00
90e76bc8f5 Merge pull request 'Update docker' (#159) from master into Docker
Reviewed-on: SGD-Nord/konova#159
2022-05-09 14:03:12 +02:00
5e84dfc4ae Merge pull request 'Docker_worker_enhance' (#155) from Docker_worker_enhance into Docker
Reviewed-on: SGD-Nord/konova#155
2022-05-09 11:05:33 +02:00
615eb65534 Docker enhancement
* optimizes image build dependency
* increases gunicorn default number of workers
2022-04-26 10:09:58 +02:00
bcde400096 WIP: Docker enhancement
* reduces all needed containers into a single one
* simplifies initial startup command by adding docker-entrypoint.sh
2022-04-26 08:55:07 +02:00
aa02dbab96 WIP: Docker enhancement
* reduce containers into a single one, holding nginx + celery + redis all at once
2022-04-25 16:07:38 +02:00
8b7c4a82aa Merge pull request 'master' (#147) from master into Docker
Reviewed-on: SGD-Nord/konova#147
2022-04-13 08:48:19 +02:00
b668c562dd Merge pull request 'master' (#136) from master into Docker
Reviewed-on: SGD-Nord/konova#136
2022-03-21 12:21:50 +01:00
460011a5e8 Docker worker enhancement
* drops docker worker process in favor of background celery worker on main process
* changes uploaded files folder into host-based folder
2022-03-09 14:03:50 +01:00
76e018e084 Merge pull request 'master' (#134) from master into Docker
Reviewed-on: SGD-Nord/konova#134
2022-03-04 13:33:35 +01:00
e00b050c8b Merge pull request 'master' (#130) from master into Docker
Reviewed-on: SGD-Nord/konova#130
2022-02-25 13:07:59 +01:00
ab9023aad0 Merge pull request 'HOTFIX' (#124) from master into Docker
Reviewed-on: SGD-Nord/konova#124
2022-02-18 15:28:35 +01:00
0dd1ac70d1 Merge pull request 'master' (#123) from master into Docker
Reviewed-on: SGD-Nord/konova#123
2022-02-18 15:21:29 +01:00
2fcc41bf4a Merge pull request 'master' (#113) from master into Docker
Reviewed-on: SGD-Nord/konova#113
2022-02-11 16:10:47 +01:00
b4e75fa2cd Revert "Revert accidental docker->master merge"
This reverts commit db834b581e.
2022-02-04 15:17:08 +01:00
409 changed files with 18889 additions and 11622 deletions

48
.env.sample Normal file
View File

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

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
# Project exclude paths # Project exclude paths
/venv/ /venv/
/.idea/ /.idea/
/.coverage
/htmlcov/
/.env

View File

@ -13,6 +13,7 @@ from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID
from konova.forms import BaseForm from konova.forms import BaseForm
from konova.utils import validators
class TimespanReportForm(BaseForm): class TimespanReportForm(BaseForm):
@ -22,6 +23,8 @@ class TimespanReportForm(BaseForm):
date_from = forms.DateField( date_from = forms.DateField(
label_suffix="", label_suffix="",
label=_("From"), label=_("From"),
validators=[validators.reasonable_date],
help_text=_("Entries created from..."),
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
"type": "date", "type": "date",
@ -34,6 +37,8 @@ class TimespanReportForm(BaseForm):
date_to = forms.DateField( date_to = forms.DateField(
label_suffix="", label_suffix="",
label=_("To"), label=_("To"),
validators=[validators.reasonable_date],
help_text=_("Entries created until..."),
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
"type": "date", "type": "date",
@ -53,7 +58,7 @@ class TimespanReportForm(BaseForm):
code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID], code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID],
), ),
widget=autocomplete.ModelSelect2( widget=autocomplete.ModelSelect2(
url="codes-conservation-office-autocomplete", url="codelist:conservation-office-autocomplete",
attrs={ attrs={
"data-placeholder": _("Click for selection") "data-placeholder": _("Click for selection")
} }

View File

@ -9,4 +9,8 @@ Created on: 19.10.21
# Defines the date of the legal publishing of the LKompVzVo # Defines the date of the legal publishing of the LKompVzVo
from django.utils import timezone from django.utils import timezone
LKOMPVZVO_PUBLISH_DATE = timezone.make_aware(timezone.datetime.fromisoformat("2018-06-16")) LKOMPVZVO_PUBLISH_DATE = timezone.make_aware(
timezone.datetime.fromisoformat(
"2018-06-16"
)
).date()

View File

@ -31,6 +31,6 @@
{% include 'analysis/reports/includes/intervention/card_intervention.html' %} {% include 'analysis/reports/includes/intervention/card_intervention.html' %}
{% include 'analysis/reports/includes/compensation/card_compensation.html' %} {% include 'analysis/reports/includes/compensation/card_compensation.html' %}
{% include 'analysis/reports/includes/eco_account/card_eco_account.html' %} {% include 'analysis/reports/includes/eco_account/card_eco_account.html' %}
{% include 'analysis/reports/includes/old_data/card_old_interventions.html' %} {% include 'analysis/reports/includes/old_data/card_old_data.html' %}
</div> </div>
{% endblock %} {% endblock %}

View File

@ -15,40 +15,40 @@
<thead> <thead>
<tr> <tr>
<th scope="col">{% trans 'Area of responsibility' %}</th> <th scope="col">{% trans 'Area of responsibility' %}</th>
<th scope="col">{% trans 'Total' %}</th>
<th scope="col">{% trans 'Number single areas' %}</th>
<th scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th> <th scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th> <th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col">{% trans 'Number single areas' %}</th>
<th scope="col">{% trans 'Total' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{% trans 'Conservation office by law' %}</td> <td>{% trans 'Conservation office by law' %}</td>
<td>{{report.compensation_report.queryset_registration_office_unb_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_unb|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_unb_checked_count|default_if_zero:"-"}}</td> <td>{{report.compensation_report.queryset_registration_office_unb_checked_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_unb_recorded_count|default_if_zero:"-"}}</td> <td>{{report.compensation_report.queryset_registration_office_unb_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_unb|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_unb_count|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Land-use planning' %}</td> <td>{% trans 'Land-use planning' %}</td>
<td>{{report.compensation_report.queryset_registration_office_tbp_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_tbp|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_tbp_checked_count|default_if_zero:"-"}}</td> <td>{{report.compensation_report.queryset_registration_office_tbp_checked_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_tbp_recorded_count|default_if_zero:"-"}}</td> <td>{{report.compensation_report.queryset_registration_office_tbp_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_tbp|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_tbp_count|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Other registration office' %}</td> <td>{% trans 'Other registration office' %}</td>
<td>{{report.compensation_report.queryset_registration_office_other_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_other|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_other_checked_count|default_if_zero:"-"}}</td> <td>{{report.compensation_report.queryset_registration_office_other_checked_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_other_recorded_count|default_if_zero:"-"}}</td> <td>{{report.compensation_report.queryset_registration_office_other_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.num_single_surfaces_total_other|default_if_zero:"-"}}</td>
<td>{{report.compensation_report.queryset_registration_office_other_count|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<td><strong>{% trans 'Total' %}</strong></td> <td><strong>{% trans 'Total' %}</strong></td>
<td><strong>{{report.compensation_report.queryset_count|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.compensation_report.num_single_surfaces_total|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.compensation_report.queryset_checked_count|default_if_zero:"-"}}</strong></td> <td><strong>{{report.compensation_report.queryset_checked_count|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.compensation_report.queryset_recorded_count|default_if_zero:"-"}}</strong></td> <td><strong>{{report.compensation_report.queryset_recorded_count|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.compensation_report.num_single_surfaces_total|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.compensation_report.queryset_count|default_if_zero:"-"}}</strong></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -10,6 +10,7 @@
{% fa5_icon 'leaf' %} {% fa5_icon 'leaf' %}
{% trans 'Compensations' %} {% trans 'Compensations' %}
</h5> </h5>
<span>{% trans 'Binding date after' %} 16.06.2018</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -10,14 +10,14 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col" class="w-25">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col">{% trans 'Total' %}</th> <th scope="col">{% trans 'Total' %}</th>
<th scope="col" class="w-25">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{{report.eco_account_report.queryset_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.eco_account_report.queryset_count|default_if_zero:"-"}}</td> <td>{{report.eco_account_report.queryset_count|default_if_zero:"-"}}</td>
<td>{{report.eco_account_report.queryset_recorded_count|default_if_zero:"-"}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -10,6 +10,7 @@
{% fa5_icon 'tree' %} {% fa5_icon 'tree' %}
{% trans 'Eco-Accounts' %} {% trans 'Eco-Accounts' %}
</h5> </h5>
<span>{% trans 'Binding date after' %} 16.06.2018</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,22 +1,37 @@
{% load i18n fontawesome_5 ksp_filters %} {% load i18n fontawesome_5 ksp_filters %}
<h3>{% trans 'Deductions' %}</h3> <h3>{% trans 'Deductions' %}</h3>
<strong>
{% blocktrans %}
Recorded = Counts the deductions whose interventions have been recorded
{% endblocktrans %}
</strong>
<div class="table-container"> <div class="table-container">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %} {% trans 'Surface' %}</th>
<th scope="col" class="w-25">{% trans 'Total' %}</th> <th scope="col" class="w-25">{% trans 'Total' %}</th>
<th scope="col" class="w-25">{% trans 'Total' %} {% trans 'Surface' %}</th> <th scope="col" class="w-25">{% trans 'Total' %} {% trans 'Surface' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %} {% trans 'Surface' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{{report.eco_account_report.queryset_deductions_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.eco_account_report.recorded_deductions_sq_m|default_if_zero:"-"}}m²</td>
<td>{{report.eco_account_report.queryset_deductions_count|default_if_zero:"-"}}</td> <td>{{report.eco_account_report.queryset_deductions_count|default_if_zero:"-"}}</td>
<td>{{report.eco_account_report.deductions_sq_m|default_if_zero:"-"}}m²</td> <td>
{{report.eco_account_report.deductions_sq_m|default_if_zero:"-"}}
{% if report.eco_account_report.deductions_sq_m > 0 %}
{% endif %}
</td>
<td>{{report.eco_account_report.queryset_deductions_recorded_count|default_if_zero:"-"}}</td>
<td>
{{report.eco_account_report.recorded_deductions_sq_m|default_if_zero:"-"}}
{% if report.eco_account_report.recorded_deductions_sq_m > 0 %}
{% endif %}
</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -14,16 +14,16 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col" class="w-25">{% trans 'Total' %}</th>
<th scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th> <th scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th> <th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col" class="w-25">{% trans 'Total' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{{report.intervention_report.queryset_count|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.queryset_checked_count|default_if_zero:"-"}}</td> <td>{{report.intervention_report.queryset_checked_count|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.queryset_recorded_count|default_if_zero:"-"}}</td> <td>{{report.intervention_report.queryset_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.queryset_count|default_if_zero:"-"}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -9,6 +9,7 @@
{% fa5_icon 'pencil-ruler' %} {% fa5_icon 'pencil-ruler' %}
{% trans 'Interventions' %} {% trans 'Interventions' %}
</h5> </h5>
<span>{% trans 'Binding date after' %} 16.06.2018</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -5,29 +5,29 @@
<thead> <thead>
<tr> <tr>
<th class="w-25" scope="col">{% trans 'Compensation type' %}</th> <th class="w-25" scope="col">{% trans 'Compensation type' %}</th>
<th class="w-25" scope="col">{% trans 'Total' %}</th>
<th class="w-25" scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th> <th class="w-25" scope="col">{% fa5_icon 'star' %} {% trans 'Checked' %}</th>
<th class="w-25" scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th> <th class="w-25" scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th class="w-25" scope="col">{% trans 'Total' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<th>{% trans 'Compensation' %}</th> <th>{% trans 'Compensation' %}</th>
<td>{{report.intervention_report.compensation_sum|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.compensation_sum_checked|default_if_zero:"-"}}</td> <td>{{report.intervention_report.compensation_sum_checked|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.compensation_sum_recorded|default_if_zero:"-"}}</td> <td>{{report.intervention_report.compensation_sum_recorded|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.compensation_sum|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<th>{% trans 'Payment' %}</th> <th>{% trans 'Payment' %}</th>
<td>{{report.intervention_report.payment_sum|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.payment_sum_checked|default_if_zero:"-"}}</td> <td>{{report.intervention_report.payment_sum_checked|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.payment_sum_recorded|default_if_zero:"-"}}</td> <td>{{report.intervention_report.payment_sum_recorded|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.payment_sum|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<th>{% trans 'Deductions' %}</th> <th>{% trans 'Deductions' %}</th>
<td>{{report.intervention_report.deduction_sum|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.deduction_sum_checked|default_if_zero:"-"}}</td> <td>{{report.intervention_report.deduction_sum_checked|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.deduction_sum_recorded|default_if_zero:"-"}}</td> <td>{{report.intervention_report.deduction_sum_recorded|default_if_zero:"-"}}</td>
<td>{{report.intervention_report.deduction_sum|default_if_zero:"-"}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -13,15 +13,15 @@
<th class="w-25" scope="col"> <th class="w-25" scope="col">
{% trans 'Law' %} {% trans 'Law' %}
</th> </th>
<th scope="col">
{% trans 'Total' %}
</th>
<th scope="col"> <th scope="col">
{% fa5_icon 'star' %} {% trans 'Checked' %} {% fa5_icon 'star' %} {% trans 'Checked' %}
</th> </th>
<th scope="col"> <th scope="col">
{% fa5_icon 'bookmark' %} {% trans 'Recorded' %} {% fa5_icon 'bookmark' %} {% trans 'Recorded' %}
</th> </th>
<th scope="col">
{% trans 'Total' %}
</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -34,16 +34,16 @@
{{law.long_name}} {{law.long_name}}
</small> </small>
</td> </td>
<td>{{law.num|default_if_zero:"-"}}</td>
<td>{{law.num_checked|default_if_zero:"-"}}</td> <td>{{law.num_checked|default_if_zero:"-"}}</td>
<td>{{law.num_recorded|default_if_zero:"-"}}</td> <td>{{law.num_recorded|default_if_zero:"-"}}</td>
<td>{{law.num|default_if_zero:"-"}}</td>
</tr> </tr>
{% endfor %} {% endfor %}
<tr> <tr>
<td><strong>{% trans 'Total' %}</strong></td> <td><strong>{% trans 'Total' %}</strong></td>
<td><strong>{{report.intervention_report.law_sum|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.intervention_report.law_sum_checked|default_if_zero:"-"}}</strong></td> <td><strong>{{report.intervention_report.law_sum_checked|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.intervention_report.law_sum_recorded|default_if_zero:"-"}}</strong></td> <td><strong>{{report.intervention_report.law_sum_recorded|default_if_zero:"-"}}</strong></td>
<td><strong>{{report.intervention_report.law_sum|default_if_zero:"-"}}</strong></td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -14,26 +14,26 @@
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
<th scope="col" class="w-25">{% fa5_icon 'star' %} {% trans 'Type' %}</th> <th scope="col" class="w-25">{% trans 'Type' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
<th scope="col">{% trans 'Total' %}</th> <th scope="col">{% trans 'Total' %}</th>
<th scope="col">{% fa5_icon 'bookmark' %} {% trans 'Recorded' %}</th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr> <tr>
<td>{% trans 'Intervention' %}</td> <td>{% trans 'Intervention' %}</td>
<td>{{report.old_data_report.queryset_intervention_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_intervention_count|default_if_zero:"-"}}</td> <td>{{report.old_data_report.queryset_intervention_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_intervention_recorded_count|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Compensation' %}</td> <td>{% trans 'Compensation' %}</td>
<td>{{report.old_data_report.queryset_comps_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_comps_count|default_if_zero:"-"}}</td> <td>{{report.old_data_report.queryset_comps_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_comps_recorded_count|default_if_zero:"-"}}</td>
</tr> </tr>
<tr> <tr>
<td>{% trans 'Eco-account' %}</td> <td>{% trans 'Eco-account' %}</td>
<td>{{report.old_data_report.queryset_acc_recorded_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_acc_count|default_if_zero:"-"}}</td> <td>{{report.old_data_report.queryset_acc_count|default_if_zero:"-"}}</td>
<td>{{report.old_data_report.queryset_acc_recorded_count|default_if_zero:"-"}}</td>
</tr> </tr>
</tbody> </tbody>
</table> </table>

View File

@ -10,7 +10,7 @@
{% fa5_icon 'pencil-ruler' %} {% fa5_icon 'pencil-ruler' %}
{% trans 'Old interventions' %} {% trans 'Old interventions' %}
</h5> </h5>
<span>{% trans 'Before' %} 16.06.2018</span> <span>{% trans 'Binding date before' %} 16.06.2018</span>
</div> </div>
</div> </div>
</div> </div>

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 15.08.23
"""

View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 15.08.23
"""

View File

@ -0,0 +1,47 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 15.08.23
"""
from datetime import timedelta
from django.urls import reverse
from django.utils.timezone import now
from django.utils.translation import gettext_lazy as _
from analysis.forms import TimespanReportForm
from konova.tests.test_views import BaseTestCase
class TimeSpanReportFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
eiv = self.create_dummy_intervention()
def test_init(self):
form = TimespanReportForm()
self.assertEqual(form.form_title, str(_("Generate report")))
self.assertEqual(form.form_caption, str(_("Select a timespan and the desired conservation office") ))
self.assertEqual(form.action_url, reverse("analysis:reports"))
self.assertFalse(form.show_cancel_btn)
self.assertEqual(form.action_btn_label, str(_("Continue")))
def test_save(self):
date_from = now().date() - timedelta(days=365)
date_to = now().date()
office = self.get_conservation_office_code()
data = {
"date_from": date_from,
"date_to": date_to,
"conservation_office": office,
}
form = TimespanReportForm(data)
self.assertTrue(form.is_valid(), msg=f"{form.errors}")
detail_report_url = form.save()
self.assertEqual(
detail_report_url,
reverse("analysis:report-detail", args=(office.id,)) + f"?df={date_from}&dt={date_to}"
)

View File

@ -0,0 +1,98 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 17.08.23
"""
from datetime import timedelta
from django.utils.timezone import now
from analysis.settings import LKOMPVZVO_PUBLISH_DATE
from analysis.utils.report import TimespanReport
from konova.sub_settings.django_settings import DEFAULT_DATE_FORMAT
from konova.tests.test_views import BaseTestCase
class TimeSpanReportTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
today = now().date()
old_date = LKOMPVZVO_PUBLISH_DATE - timedelta(days=1)
self.conservation_office = self.get_conservation_office_code()
self.eiv_old = self.create_dummy_intervention()
self.kom_old = self.create_dummy_compensation(interv=self.eiv_old)
self.assertNotEqual(self.compensation.intervention, self.kom_old.intervention)
self.eiv = self.compensation.intervention
self.oek_old = self.create_dummy_eco_account()
self.eiv_old.responsible.conservation_office = self.conservation_office
self.eiv_old.legal.binding_date = old_date
self.eiv_old.legal.registration_date = old_date
self.eiv.responsible.conservation_office = self.conservation_office
self.eiv.legal.binding_date = today
self.eiv.legal.registration_date = today
self.eco_account.responsible.conservation_office = self.conservation_office
self.eco_account.legal.registration_date = today
self.eco_account.legal.binding_date = today
self.oek_old.responsible.conservation_office = self.conservation_office
self.oek_old.legal.registration_date = old_date
self.oek_old.legal.binding_date = old_date
self.eiv.legal.save()
self.eiv.responsible.save()
self.eiv_old.legal.save()
self.eiv_old.responsible.save()
self.eco_account.legal.save()
self.eco_account.responsible.save()
self.oek_old.legal.save()
self.oek_old.responsible.save()
self.deduction.account = self.eco_account
self.deduction.intervention = self.eiv
self.deduction.save()
def test_init(self):
date_from = now().date() - timedelta(days=365)
date_to = now().date()
report = TimespanReport(self.conservation_office.id, date_from, date_to)
self.assertEqual(report.office_id, self.conservation_office.id)
self.assertEqual(report.date_from, date_from)
self.assertEqual(report.date_to, date_to)
self.assertIsNotNone(report.intervention_report)
self.assertIsNotNone(report.compensation_report)
self.assertIsNotNone(report.eco_account_report)
self.assertIsNotNone(report.old_data_report)
self.assertEqual(report.excel_map["date_from"], date_from.strftime(DEFAULT_DATE_FORMAT))
self.assertEqual(report.excel_map["date_to"], date_to.strftime(DEFAULT_DATE_FORMAT))
self.assertEqual(report.old_data_report.queryset_intervention_count, 1)
self.assertEqual(report.old_data_report.queryset_intervention_recorded_count, 0)
self.assertEqual(report.old_data_report.queryset_comps_count, 1)
self.assertEqual(report.old_data_report.queryset_acc_count, 1)
self.assertEqual(report.old_data_report.queryset_acc_recorded_count, 0)
self.assertEqual(report.intervention_report.queryset_count, 1)
self.assertEqual(report.intervention_report.queryset_checked_count, 0)
self.assertEqual(report.intervention_report.queryset_recorded_count, 0)
self.assertEqual(report.compensation_report.queryset_count, 1)
self.assertEqual(report.compensation_report.queryset_checked_count, 0)
self.assertEqual(report.compensation_report.queryset_recorded_count, 0)
self.assertEqual(report.eco_account_report.queryset_count, 1)
self.assertEqual(report.eco_account_report.queryset_recorded_count, 0)
self.assertEqual(report.eco_account_report.queryset_deductions_count, 1)
self.assertEqual(report.eco_account_report.queryset_deductions_recorded_count, 0)

View File

@ -108,7 +108,7 @@ class TempExcelFile:
for _iter_entry in _iter_obj: for _iter_entry in _iter_obj:
j = 0 j = 0
for _iter_attr in _attrs: for _iter_attr in _attrs:
_new_cell = ws.cell(start_cell.row + i, start_cell.column + j, getattr(_iter_entry, _iter_attr)) _new_cell = ws.cell(start_cell.row + i, start_cell.column + j, _iter_entry.get(_iter_attr, "MISSING"))
_new_cell.border = border_style _new_cell.border = border_style
j += 1 j += 1
i += 1 i += 1

View File

@ -17,6 +17,7 @@ from compensation.models import Compensation, Payment, EcoAccountDeduction, EcoA
from intervention.models import Intervention from intervention.models import Intervention
from konova.models import Geometry from konova.models import Geometry
from konova.sub_settings.django_settings import BASE_DIR, DEFAULT_DATE_FORMAT from konova.sub_settings.django_settings import BASE_DIR, DEFAULT_DATE_FORMAT
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
class TimespanReport: class TimespanReport:
@ -103,9 +104,9 @@ class TimespanReport:
"iterable": self.evaluated_laws, "iterable": self.evaluated_laws,
"attrs": [ "attrs": [
"short_name", "short_name",
"num",
"num_checked", "num_checked",
"num_recorded", "num_recorded",
"num",
] ]
}, },
"i_laws_checked": self.law_sum_checked, "i_laws_checked": self.law_sum_checked,
@ -137,22 +138,36 @@ class TimespanReport:
).order_by( ).order_by(
"long_name" "long_name"
) )
# Fetch all law ids which are used by any .legal object of an intervention object
intervention_laws_total = self.queryset.values_list("legal__laws__id") evaluated_laws = []
intervention_laws_checked = self.queryset.filter(checked__isnull=False).values_list("legal__laws__id") sum_num_checked = 0
intervention_laws_recorded = self.queryset.filter(recorded__isnull=False).values_list( sum_num_recorded = 0
"legal__laws__id") sum_num = 0
# Count how often which law id appears in the above list, return only the long_name of the law and the resulting for law in laws:
# count (here 'num'). This is for keeping the db fetch as small as possible num = self.queryset.filter(
# Compute the sum for total, checked and recorded legal__laws__atom_id=law.atom_id
self.evaluated_laws = laws.annotate( ).count()
num=Count("id", filter=Q(id__in=intervention_laws_total)), num_checked = self.queryset_checked.filter(
num_checked=Count("id", filter=Q(id__in=intervention_laws_checked)), legal__laws__atom_id=law.atom_id
num_recorded=Count("id", filter=Q(id__in=intervention_laws_recorded)), ).count()
).values_list("short_name", "long_name", "num_checked", "num_recorded", "num", named=True) num_recorded = self.queryset_recorded.filter(
self.law_sum = self.evaluated_laws.aggregate(sum_num=Sum("num"))["sum_num"] legal__laws__atom_id=law.atom_id
self.law_sum_checked = self.evaluated_laws.aggregate(sum_num_checked=Sum("num_checked"))["sum_num_checked"] ).count()
self.law_sum_recorded = self.evaluated_laws.aggregate(sum_num_recorded=Sum("num_recorded"))["sum_num_recorded"] evaluated_laws.append({
"short_name": law.short_name,
"long_name": law.long_name,
"num": num,
"num_checked": num_checked,
"num_recorded": num_recorded,
})
sum_num += num
sum_num_checked += num_checked
sum_num_recorded += num_recorded
self.evaluated_laws = evaluated_laws
self.law_sum = sum_num
self.law_sum_checked = sum_num_checked
self.law_sum_recorded = sum_num_recorded
def _evaluate_compensations(self): def _evaluate_compensations(self):
""" Analyzes the types of compensation distribution """ Analyzes the types of compensation distribution
@ -319,7 +334,7 @@ class TimespanReport:
return Geometry.objects.filter( return Geometry.objects.filter(
id__in=ids id__in=ids
).annotate( ).annotate(
geom_cast=Cast("geom", MultiPolygonField()) geom_cast=Cast("geom", MultiPolygonField(srid=DEFAULT_SRID_RLP))
).annotate( ).annotate(
num=NumGeometries("geom_cast") num=NumGeometries("geom_cast")
).aggregate( ).aggregate(
@ -398,6 +413,7 @@ class TimespanReport:
def __init__(self, id: str, date_from: str, date_to: str): def __init__(self, id: str, date_from: str, date_to: str):
# First fetch all eco account for this office # First fetch all eco account for this office
self.queryset = EcoAccount.objects.filter( self.queryset = EcoAccount.objects.filter(
legal__registration_date__gt=LKOMPVZVO_PUBLISH_DATE,
responsible__conservation_office__id=id, responsible__conservation_office__id=id,
deleted=None, deleted=None,
created__timestamp__date__gte=date_from, created__timestamp__date__gte=date_from,
@ -501,8 +517,8 @@ class TimespanReport:
legal__registration_date__lte=LKOMPVZVO_PUBLISH_DATE, legal__registration_date__lte=LKOMPVZVO_PUBLISH_DATE,
responsible__conservation_office__id=id, responsible__conservation_office__id=id,
deleted=None, deleted=None,
created__timestamp__gte=date_from, created__timestamp__date__gte=date_from,
created__timestamp__lte=date_to, created__timestamp__date__lte=date_to,
) )
self.queryset_acc_recorded = self.queryset_acc.filter( self.queryset_acc_recorded = self.queryset_acc.filter(
recorded__isnull=False, recorded__isnull=False,

View File

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

View File

@ -0,0 +1,18 @@
# Generated by Django 4.2.6 on 2023-11-30 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('api', '0001_initial'),
]
operations = [
migrations.AlterField(
model_name='apiusertoken',
name='valid_until',
field=models.DateField(blank=True, help_text='Token is only valid until this date. Forever if null/blank.', null=True),
),
]

View File

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

View File

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

View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 17.08.23
"""

View File

@ -0,0 +1,71 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 17.08.23
"""
from datetime import timedelta
from django.utils.timezone import now
from api.models import APIUserToken
from konova.tests.test_views import BaseTestCase
class APIUserTokenTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.token = APIUserToken.objects.create()
self.superuser.api_token = self.token
self.superuser.save()
def test_str(self):
self.assertEqual(str(self.token), self.token.token)
def test_get_user_from_token(self):
a_day = timedelta(days=1)
today = now().date()
self.assertFalse(self.token.is_active)
self.assertIsNone(self.token.valid_until)
try:
#Token not existing --> fail
token_user = APIUserToken.get_user_from_token(self.token.token[::-1])
self.fail("There should not have been any token")
except PermissionError:
pass
try:
# Token not active --> fail
token_user = APIUserToken.get_user_from_token(self.token.token)
self.fail("Token is unverified but token user has been fetchable.")
except PermissionError:
pass
self.token.is_active = True
self.token.valid_until = today - a_day
self.token.save()
try:
# Token valid until yesterday --> fail
token_user = APIUserToken.get_user_from_token(self.token.token)
self.fail("Token reached end of lifetime but token user has been fetchable.")
except PermissionError:
pass
# Token valid until tomorrow --> success
self.token.valid_until = today + a_day
self.token.save()
token_user = APIUserToken.get_user_from_token(self.token.token)
self.assertEqual(token_user, self.superuser)
del token_user
# Token valid forever --> success
self.token.valid_until = None
self.token.save()
token_user = APIUserToken.get_user_from_token(self.token.token)
self.assertEqual(token_user, self.superuser)

View File

@ -1,5 +1,5 @@
{ {
"eco_account": "CHANGE_BEFORE_RUN!!!", "eco_account": "CHANGE_BEFORE_RUN!!!",
"surface": 500.0, "surface": 500.50,
"intervention": "CHANGE_BEFORE_RUN!!!" "intervention": "CHANGE_BEFORE_RUN!!!"
} }

View File

@ -4,7 +4,6 @@ from django.urls import reverse
from konova.settings import DEFAULT_GROUP from konova.settings import DEFAULT_GROUP
from konova.tests.test_views import BaseTestCase from konova.tests.test_views import BaseTestCase
from konova.utils.user_checks import is_default_group_only
class BaseAPIV1TestCase(BaseTestCase): class BaseAPIV1TestCase(BaseTestCase):
@ -138,7 +137,7 @@ class APIV1SharingTestCase(BaseAPIV1TestCase):
# Give the user only default group rights # Give the user only default group rights
default_group = self.groups.get(name=DEFAULT_GROUP) default_group = self.groups.get(name=DEFAULT_GROUP)
self.superuser.groups.set([default_group]) self.superuser.groups.set([default_group])
self.assertTrue(is_default_group_only(self.superuser)) self.assertTrue(self.superuser.is_default_group_only())
# Add only him as shared_users an object # Add only him as shared_users an object
self.intervention.users.set([self.superuser]) self.intervention.users.set([self.superuser])

View File

@ -1,5 +1,5 @@
{ {
"eco_account": "CHANGE_BEFORE_RUN!!!", "eco_account": "CHANGE_BEFORE_RUN!!!",
"surface": 523400.0, "surface": 523400.50,
"intervention": "CHANGE_BEFORE_RUN!!!" "intervention": "CHANGE_BEFORE_RUN!!!"
} }

View File

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

View File

@ -11,7 +11,9 @@ from abc import abstractmethod
from django.contrib.gis import geos from django.contrib.gis import geos
from django.contrib.gis.geos import GEOSGeometry from django.contrib.gis.geos import GEOSGeometry
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Q
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
@ -31,8 +33,8 @@ class AbstractModelAPISerializer:
self.lookup = { self.lookup = {
"id": None, # must be set "id": None, # must be set
"deleted__isnull": True, "deleted__isnull": True,
"users__in": [], # must be set
} }
self.shared_lookup = Q() # must be set, so user or team based share will be used properly
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
@abstractmethod @abstractmethod
@ -75,7 +77,11 @@ class AbstractModelAPISerializer:
else: else:
# Return certain object # Return certain object
self.lookup["id"] = _id self.lookup["id"] = _id
self.lookup["users__in"] = [user]
self.shared_lookup = Q(
Q(users__in=[user]) |
Q(teams__in=list(user.shared_teams))
)
def fetch_and_serialize(self): def fetch_and_serialize(self):
""" Serializes the model entry according to the given lookup data """ Serializes the model entry according to the given lookup data
@ -85,7 +91,13 @@ class AbstractModelAPISerializer:
Returns: Returns:
serialized_data (dict) serialized_data (dict)
""" """
entries = self.model.objects.filter(**self.lookup).order_by("id") entries = self.model.objects.filter(
**self.lookup
).filter(
self.shared_lookup
).order_by(
"id"
).distinct()
self.paginator = Paginator(entries, self.rpp) self.paginator = Paginator(entries, self.rpp)
requested_entries = self.paginator.page(self.page_number) requested_entries = self.paginator.page(self.page_number)
@ -133,8 +145,8 @@ class AbstractModelAPISerializer:
if isinstance(geojson, dict): if isinstance(geojson, dict):
geojson = json.dumps(geojson) geojson = json.dumps(geojson)
geometry = geos.fromstr(geojson) geometry = geos.fromstr(geojson)
if geometry.empty: if geometry.srid != DEFAULT_SRID_RLP:
geometry = None geometry.transform(DEFAULT_SRID_RLP)
return geometry return geometry
def _get_obj_from_db(self, id, user): def _get_obj_from_db(self, id, user):

View File

@ -6,12 +6,13 @@ Created on: 24.01.22
""" """
from django.db import transaction from django.db import transaction
from django.db.models import Q
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin
from compensation.models import Compensation from compensation.models import Compensation
from intervention.models import Intervention from intervention.models import Intervention
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -21,8 +22,10 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
def prepare_lookup(self, id, user): def prepare_lookup(self, id, user):
super().prepare_lookup(id, user) super().prepare_lookup(id, user)
del self.lookup["users__in"] self.shared_lookup = Q(
self.lookup["intervention__users__in"] = [user] Q(intervention__users__in=[user]) |
Q(intervention__teams__in=user.shared_teams)
)
def intervention_to_json(self, entry): def intervention_to_json(self, entry):
return { return {
@ -64,6 +67,7 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj = Compensation() obj = Compensation()
created = create_action created = create_action
obj.created = created obj.created = created
obj.modified = created
obj.geometry = geometry obj.geometry = geometry
return obj return obj
@ -125,11 +129,12 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states) obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
obj = self._set_deadlines(obj, properties["deadlines"]) obj = self._set_deadlines(obj, properties["deadlines"])
obj.log.add(obj.created) obj.log.add(obj.created)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
celery_check_for_geometry_conflicts.delay(obj.geometry.id)
return obj.id return obj.id
def update_model_from_json(self, id, json_model, user): def update_model_from_json(self, id, json_model, user):
""" Updates an entry for the model based on the contents of json_model """ Updates an entry for the model based on the contents of json_model
@ -165,8 +170,8 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states) obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
obj = self._set_deadlines(obj, properties["deadlines"]) obj = self._set_deadlines(obj, properties["deadlines"])
obj.log.add(update_action) obj.log.add(update_action)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
return obj.id return obj.id

View File

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

View File

@ -13,7 +13,7 @@ from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_
from compensation.models import EcoAccount from compensation.models import EcoAccount
from intervention.models import Legal, Responsibility, Handler from intervention.models import Legal, Responsibility, Handler
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -103,6 +103,7 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj.legal = Legal() obj.legal = Legal()
created = create_action created = create_action
obj.created = created obj.created = created
obj.modified = created
obj.geometry = geometry obj.geometry = geometry
return obj return obj
@ -146,12 +147,13 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states) obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
obj = self._set_deadlines(obj, properties["deadlines"]) obj = self._set_deadlines(obj, properties["deadlines"])
obj.log.add(obj.created) obj.log.add(obj.created)
obj.users.add(user) obj.users.add(user)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
celery_check_for_geometry_conflicts.delay(obj.geometry.id)
return obj.id return obj.id
def update_model_from_json(self, id, json_model, user): def update_model_from_json(self, id, json_model, user):
""" Updates an entry for the model based on the contents of json_model """ Updates an entry for the model based on the contents of json_model
@ -190,8 +192,8 @@ class EcoAccountAPISerializerV1(AbstractModelAPISerializerV1,
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states) obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
obj = self._set_deadlines(obj, properties["deadlines"]) obj = self._set_deadlines(obj, properties["deadlines"])
obj.log.add(update_action) obj.log.add(update_action)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
return obj.id return obj.id

View File

@ -13,7 +13,7 @@ from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_
from ema.models import Ema from ema.models import Ema
from intervention.models import Responsibility, Handler from intervention.models import Responsibility, Handler
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -85,6 +85,7 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
) )
created = create_action created = create_action
obj.created = created obj.created = created
obj.modified = created
obj.geometry = geometry obj.geometry = geometry
return obj return obj
@ -118,12 +119,13 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states) obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
obj = self._set_deadlines(obj, properties["deadlines"]) obj = self._set_deadlines(obj, properties["deadlines"])
obj.log.add(obj.created) obj.log.add(obj.created)
obj.users.add(user) obj.users.add(user)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
celery_check_for_geometry_conflicts.delay(obj.geometry.id)
return obj.id return obj.id
def update_model_from_json(self, id, json_model, user): def update_model_from_json(self, id, json_model, user):
""" Updates an entry for the model based on the contents of json_model """ Updates an entry for the model based on the contents of json_model
@ -159,8 +161,8 @@ class EmaAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensationAPISe
obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states) obj = self._set_compensation_states(obj, properties["after_states"], obj.after_states)
obj = self._set_deadlines(obj, properties["deadlines"]) obj = self._set_deadlines(obj, properties["deadlines"])
obj.log.add(update_action) obj.log.add(update_action)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
return obj.id return obj.id

View File

@ -13,7 +13,7 @@ from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, \
from compensation.models import Payment from compensation.models import Payment
from intervention.models import Intervention, Responsibility, Legal, Handler from intervention.models import Intervention, Responsibility, Legal, Handler
from konova.models import Geometry from konova.models import Geometry
from konova.tasks import celery_update_parcels from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@ -76,6 +76,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
created = create_action created = create_action
obj.legal = legal obj.legal = legal
obj.created = created obj.created = created
obj.modified = created
obj.geometry = geometry obj.geometry = geometry
obj.responsible = resp obj.responsible = resp
return obj return obj
@ -132,7 +133,6 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
id__in=payments id__in=payments
) )
obj.payments.set(payments) obj.payments.set(payments)
obj.send_data_to_egon()
return obj return obj
def create_model_from_json(self, json_model, user): def create_model_from_json(self, json_model, user):
@ -161,12 +161,13 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
obj.legal.save() obj.legal.save()
obj.save() obj.save()
obj.users.add(user) obj.users.add(user)
obj.log.add(obj.created) obj.log.add(obj.created)
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
celery_check_for_geometry_conflicts.delay(obj.geometry.id)
return obj.id return obj.id
def update_model_from_json(self, id, json_model, user): def update_model_from_json(self, id, json_model, user):
""" Updates an entry for the model based on the contents of json_model """ Updates an entry for the model based on the contents of json_model
@ -198,8 +199,9 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
obj.legal.save() obj.legal.save()
obj.save() obj.save()
obj.mark_as_edited(user, edit_comment="API update") obj.mark_as_edited(user, edit_comment="API update")
obj.send_data_to_egon()
celery_update_parcels.delay(obj.geometry.id) celery_update_parcels.delay(obj.geometry.id)
return obj.id return obj.id

View File

@ -9,13 +9,15 @@ Created on: 24.01.22
import json import json
from django.contrib.gis.geos import MultiPolygon from django.contrib.gis.geos import MultiPolygon
from django.core.exceptions import ObjectDoesNotExist
from django.db.models import QuerySet from django.db.models import QuerySet
from api.utils.serializer.serializer import AbstractModelAPISerializer from api.utils.serializer.serializer import AbstractModelAPISerializer
from codelist.models import KonovaCode from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \ from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID, CODELIST_PROCESS_TYPE_ID, \
CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \ CODELIST_LAW_ID, CODELIST_REGISTRATION_OFFICE_ID, CODELIST_CONSERVATION_OFFICE_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, CODELIST_HANDLER_ID CODELIST_COMPENSATION_ACTION_DETAIL_ID, CODELIST_HANDLER_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from compensation.models import CompensationAction, UnitChoices, CompensationState from compensation.models import CompensationAction, UnitChoices, CompensationState
from intervention.models import Responsibility, Legal, Handler from intervention.models import Responsibility, Legal, Handler
from konova.models import Deadline, DeadlineType from konova.models import Deadline, DeadlineType
@ -60,7 +62,7 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
if konova_code is None: if konova_code is None:
return None return None
return { return {
"atom_id": konova_code.atom_id, "id": konova_code.id,
"long_name": konova_code.long_name, "long_name": konova_code.long_name,
"short_name": konova_code.short_name, "short_name": konova_code.short_name,
} }
@ -69,7 +71,7 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
""" Returns a konova code instance """ Returns a konova code instance
Args: Args:
json_str (str): The value for the code (atom id) json_str (str): The value for the code (id)
code_list_identifier (str): From which konova code list this code is supposed to be from code_list_identifier (str): From which konova code list this code is supposed to be from
Returns: Returns:
@ -80,10 +82,14 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
json_str = str(json_str) json_str = str(json_str)
if len(json_str) == 0: if len(json_str) == 0:
return None return None
code = KonovaCode.objects.get( try:
atom_id=json_str, code = KonovaCode.objects.get(
code_lists__in=[code_list_identifier] id=json_str,
) code_lists__in=[code_list_identifier]
)
except ObjectDoesNotExist as e:
msg = f"{e.args[0]} ({json_str} not found in official list {code_list_identifier})"
raise ObjectDoesNotExist(msg)
return code return code
def _created_on_to_json(self, entry): def _created_on_to_json(self, entry):
@ -292,9 +298,12 @@ class AbstractCompensationAPISerializerV1Mixin:
""" """
deadlines = [] deadlines = []
for entry in deadline_data: for entry in deadline_data:
deadline_type = entry["type"] try:
date = entry["date"] deadline_type = entry["type"]
comment = entry["comment"] date = entry["date"]
comment = entry["comment"]
except KeyError:
raise ValueError(f"Invalid deadline content. Content was {entry} but should follow the specification")
# Check on validity # Check on validity
if deadline_type not in DeadlineType: if deadline_type not in DeadlineType:
@ -336,11 +345,14 @@ class AbstractCompensationAPISerializerV1Mixin:
""" """
states = [] states = []
for entry in states_data: for entry in states_data:
biotope_type = entry["biotope"] try:
biotope_details = [ biotope_type = entry["biotope"]
self._konova_code_from_json(e, CODELIST_BIOTOPES_EXTRA_CODES_ID) for e in entry["biotope_details"] biotope_details = [
] self._konova_code_from_json(e, CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID) for e in entry["biotope_details"]
surface = float(entry["surface"]) ]
surface = float(entry["surface"])
except KeyError:
raise ValueError(f"Invalid biotope content. Content was {entry} but should follow the specification ")
# Check on validity # Check on validity
if surface <= 0: if surface <= 0:
@ -349,7 +361,7 @@ class AbstractCompensationAPISerializerV1Mixin:
# If this exact data is already existing, we do not create it new. Instead put it's id in the list of # If this exact data is already existing, we do not create it new. Instead put it's id in the list of
# entries, we will use to set the new actions # entries, we will use to set the new actions
state = states_manager.filter( state = states_manager.filter(
biotope_type__atom_id=biotope_type, biotope_type__id=biotope_type,
surface=surface, surface=surface,
).exclude( ).exclude(
id__in=states id__in=states
@ -380,15 +392,19 @@ class AbstractCompensationAPISerializerV1Mixin:
""" """
actions = [] actions = []
for entry in actions_data: for entry in actions_data:
action_types = [ try:
self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_ID) for e in entry["action_types"] action_types = [
] self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_ID) for e in entry["action_types"]
action_details = [ ]
self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_DETAIL_ID) for e in entry["action_details"] action_details = [
] self._konova_code_from_json(e, CODELIST_COMPENSATION_ACTION_DETAIL_ID) for e in entry["action_details"]
amount = float(entry["amount"]) ]
unit = entry["unit"] amount = float(entry["amount"])
comment = entry["comment"] # Mapping of old "qm" into "m²"
unit = UnitChoices.m2.value if entry["unit"] == "qm" else entry["unit"]
comment = entry["comment"]
except KeyError:
raise ValueError(f"Invalid action content. Content was {entry} but should follow specification")
# Check on validity # Check on validity
if amount <= 0: if amount <= 0:

View File

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

View File

@ -18,7 +18,6 @@ from compensation.models import EcoAccount
from ema.models import Ema from ema.models import Ema
from intervention.models import Intervention from intervention.models import Intervention
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
from konova.utils.user_checks import is_default_group_only
from user.models import User, Team from user.models import User, Team
@ -51,9 +50,20 @@ class AbstractAPIView(View):
def dispatch(self, request, *args, **kwargs): def dispatch(self, request, *args, **kwargs):
try: try:
# Fetch the proper user from the given request header token # Fetch the proper user from the given request header token
ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None) token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None) ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
self.user = APIUserToken.get_user_from_token(ksp_token, ksp_user)
if not token and not ksp_user:
bearer_token = request.headers.get("authorization", None)
if not bearer_token:
raise PermissionError("No token provided")
token = bearer_token.split(" ")[1]
token_user = APIUserToken.get_user_from_token(token)
if ksp_user and ksp_user != token_user.username:
raise PermissionError(f"Invalid token for {ksp_user}")
self.user = token_user
request.user = self.user request.user = self.user
if not self.user.is_default_user(): if not self.user.is_default_user():
raise PermissionError("Default permissions required") raise PermissionError("Default permissions required")
@ -315,7 +325,7 @@ class AbstractModelShareAPIView(AbstractAPIView):
for team_name in new_teams: for team_name in new_teams:
new_teams_objs.append(Team.objects.get(name=team_name)) new_teams_objs.append(Team.objects.get(name=team_name))
if is_default_group_only(self.user): if self.user.is_default_group_only():
# Default only users are not allowed to remove other users from having access. They can only add new ones! # Default only users are not allowed to remove other users from having access. They can only add new ones!
new_users_to_be_added = User.objects.filter( new_users_to_be_added = User.objects.filter(
username__in=new_users username__in=new_users

View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""

View File

@ -0,0 +1,74 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal_select2.views import Select2GroupQuerySetView
from django.db.models import Q
from codelist.models import KonovaCode
class KonovaCodeAutocomplete(Select2GroupQuerySetView):
"""
Provides simple autocomplete functionality for codes
Parameter support:
* q: Search for a word inside long_name of a code
* c: Search inside a special codelist
"""
paginate_by = 50
def order_by(self, qs):
""" Orders by a predefined value
Wrapped in a function to provide inheritance-based different orders
Args:
qs (QuerySet): The queryset to be ordered
Returns:
qs (QuerySet): The ordered queryset
"""
return qs.order_by(
"long_name"
)
def get_queryset(self):
if self.request.user.is_anonymous:
return KonovaCode.objects.none()
qs = KonovaCode.objects.filter(
is_archived=False,
is_selectable=True,
is_leaf=True,
)
qs = self.order_by(qs)
if self.c:
qs = qs.filter(
code_lists__in=[self.c]
)
if self.q:
# Remove whitespaces from self.q and split input in all keywords (if multiple given)
q = dict.fromkeys(self.q.strip().split(" "))
# Create one filter looking up for all keys where all keywords can be found in the same result
_filter = Q()
for keyword in q:
q_or = Q()
q_or |= Q(long_name__icontains=keyword)
q_or |= Q(short_name__icontains=keyword)
q_or |= Q(parent__long_name__icontains=keyword)
q_or |= Q(parent__short_name__icontains=keyword)
q_or |= Q(parent__parent__long_name__icontains=keyword)
q_or |= Q(parent__parent__short_name__icontains=keyword)
_filter.add(q_or, Q.AND)
qs = qs.filter(_filter).distinct()
return qs
def get_result_label(self, result):
return f"{result.long_name}"
def get_selected_result_label(self, result):
return f"{result.__str__()}"

View File

@ -0,0 +1,114 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
import collections
from django.core.exceptions import ImproperlyConfigured
from codelist.settings import CODELIST_BIOTOPES_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
from konova.utils.message_templates import UNGROUPED
class BiotopeCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_BIOTOPES_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
""" Orders by a predefined value
Wrapped in a function to provide inheritance-based different orders
Args:
qs (QuerySet): The queryset to be ordered
Returns:
qs (QuerySet): The ordered queryset
"""
return qs.order_by(
"short_name",
)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"
def get_results(self, context):
"""Return the options grouped by a common related model.
Raises ImproperlyConfigured if self.group_by_name is not configured
"""
if not self.group_by_related:
raise ImproperlyConfigured("Missing group_by_related.")
super_groups = collections.OrderedDict()
object_list = context['object_list']
for result in object_list:
group = result.parent if result.parent else None
group_name = f"{group.long_name} ({group.short_name})" if group else UNGROUPED
super_group = result.parent.parent if result.parent else None
super_group_name = f"{super_group.long_name} ({super_group.short_name})" if super_group else UNGROUPED
super_groups.setdefault(super_group_name, {})
super_groups[super_group_name].setdefault(group_name, [])
super_groups[super_group_name][group_name].append(result)
return [{
'id': None,
'text': super_group,
'children': [{
"id": None,
"text": group,
"children": [{
'id': self.get_result_value(result),
'text': self.get_result_label(result),
'selected_text': self.get_selected_result_label(result),
} for result in results]
} for group, results in groups.items()]
} for super_group, groups in super_groups.items()]
class BiotopeExtraCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "short_name"
paginate_by = 200
def __init__(self, *args, **kwargs):
self.c = CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
""" Orders by a predefined value
Wrapped in a function to provide inheritance-based different orders
Args:
qs (QuerySet): The queryset to be ordered
Returns:
qs (QuerySet): The ordered queryset
"""
return qs.order_by(
"short_name",
)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"
def get_selected_result_label(self, result):
return f"{result.parent.short_name} > {result.long_name} ({result.short_name})"

View File

@ -0,0 +1,45 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
class CompensationActionCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_COMPENSATION_ACTION_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
return qs.order_by(
"parent__long_name"
)
class CompensationActionDetailCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
paginate_by = 200
def __init__(self, *args, **kwargs):
self.c = CODELIST_COMPENSATION_ACTION_DETAIL_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
return qs.order_by(
"long_name"
)

View File

@ -0,0 +1,24 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.settings import CODELIST_HANDLER_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
class HandlerCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_HANDLER_ID
super().__init__(*args, **kwargs)
def get_result_label(self, result):
return result.long_name

View File

@ -0,0 +1,24 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.settings import CODELIST_LAW_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
class LawCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_LAW_ID
super().__init__(*args, **kwargs)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"

View File

@ -0,0 +1,41 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_REGISTRATION_OFFICE_ID
from codelist.autocomplete.base import KonovaCodeAutocomplete
class RegistrationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_REGISTRATION_OFFICE_ID
super().__init__(*args, **kwargs)
def order_by(self, qs):
return qs.order_by(
"parent__long_name"
)
class ConservationOfficeCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_CONSERVATION_OFFICE_ID
super().__init__(*args, **kwargs)
def get_result_label(self, result):
return f"{result.long_name} ({result.short_name})"

View File

@ -0,0 +1,21 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from codelist.autocomplete.base import KonovaCodeAutocomplete
from codelist.settings import CODELIST_PROCESS_TYPE_ID
class ProcessTypeCodeAutocomplete(KonovaCodeAutocomplete):
"""
Due to limitations of the django dal package, we need to subclass for each code list
"""
group_by_related = "parent"
related_field_name = "long_name"
def __init__(self, *args, **kwargs):
self.c = CODELIST_PROCESS_TYPE_ID
super().__init__(*args, **kwargs)

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -7,8 +7,24 @@ Created on: 23.08.21
""" """
from django.urls import path from django.urls import path
from codelist.autocomplete.biotope import BiotopeCodeAutocomplete, BiotopeExtraCodeAutocomplete
from codelist.autocomplete.compensation_action import CompensationActionDetailCodeAutocomplete, \
CompensationActionCodeAutocomplete
from codelist.autocomplete.handler import HandlerCodeAutocomplete
from codelist.autocomplete.law import LawCodeAutocomplete
from codelist.autocomplete.office import ConservationOfficeCodeAutocomplete, RegistrationOfficeCodeAutocomplete
from codelist.autocomplete.process_type import ProcessTypeCodeAutocomplete
app_name = "codelist" app_name = "codelist"
urlpatterns = [ urlpatterns = [
path("atcmplt/codes/biotope", BiotopeCodeAutocomplete.as_view(), name="biotope-autocomplete"),
path("atcmplt/codes/biotope/extra", BiotopeExtraCodeAutocomplete.as_view(),
name="biotope-extra-type-autocomplete"),
path("atcmplt/codes/law", LawCodeAutocomplete.as_view(), name="law-autocomplete"),
path("atcmplt/codes/reg-off", RegistrationOfficeCodeAutocomplete.as_view(), name="registration-office-autocomplete"),
path("atcmplt/codes/cons-off", ConservationOfficeCodeAutocomplete.as_view(), name="conservation-office-autocomplete"),
path("atcmplt/codes/handler", HandlerCodeAutocomplete.as_view(), name="handler-autocomplete"),
path("atcmplt/codes/comp/action", CompensationActionCodeAutocomplete.as_view(), name="compensation-action-autocomplete"),
path("atcmplt/codes/comp/action/detail", CompensationActionDetailCodeAutocomplete.as_view(), name="compensation-action-detail-autocomplete"),
path("atcmplt/codes/prc-type", ProcessTypeCodeAutocomplete.as_view(), name="process-type-autocomplete"),
] ]

View File

@ -81,13 +81,15 @@ class EcoAccountAdmin(AbstractCompensationAdmin):
] ]
filter_horizontal = [ filter_horizontal = [
"users" "users",
"teams",
] ]
def get_fields(self, request, obj=None): def get_fields(self, request, obj=None):
return super().get_fields(request, obj) + [ return super().get_fields(request, obj) + [
"deductable_surface", "deductable_surface",
"users" "users",
"teams",
] ]
@ -146,7 +148,7 @@ class CompensationActionAdmin(admin.ModelAdmin):
admin.site.register(Compensation, CompensationAdmin) admin.site.register(Compensation, CompensationAdmin)
admin.site.register(EcoAccount, EcoAccountAdmin) admin.site.register(EcoAccount, EcoAccountAdmin)
admin.site.register(EcoAccountDeduction, EcoAccountDeductionAdmin) #admin.site.register(EcoAccountDeduction, EcoAccountDeductionAdmin)
# For a more cleaner admin interface these rarely used admin views are not important for deployment # For a more cleaner admin interface these rarely used admin views are not important for deployment
#admin.site.register(Payment, PaymentAdmin) #admin.site.register(Payment, PaymentAdmin)

View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""

View File

@ -0,0 +1,40 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal_select2.views import Select2QuerySetView
from django.db.models import Q
from compensation.models import EcoAccount
class EcoAccountAutocomplete(Select2QuerySetView):
""" Autocomplete for ecoAccount entries
Only returns entries that are already recorded and not deleted
"""
def get_queryset(self):
if self.request.user.is_anonymous:
return EcoAccount.objects.none()
qs = EcoAccount.objects.filter(
deleted=None,
recorded__isnull=False,
).order_by(
"identifier"
)
if self.q:
qs = qs.filter(
Q(identifier__icontains=self.q) |
Q(title__icontains=self.q)
).distinct()
return qs
def get_result_label(self, result):
return str(result)
def get_selected_result_label(self, result):
return str(result)

View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""

View File

@ -1,17 +1,14 @@
""" """
Author: Michel Peltriaux Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 29.07.21 Created on: 18.08.22
""" """
import django_filters
from django.utils.translation import gettext_lazy as _
from django import forms
from django.db.models import QuerySet, Q from django.db.models import QuerySet, Q
from konova.filters.mixins import ConservationOfficeTableFilterMixin from konova.filters.table_filters import AbstractTableFilter, CheckboxTableFilter, QueryTableFilter, \
from konova.filters.table_filters import QueryTableFilter, CheckboxTableFilter, SelectionTableFilter, AbstractTableFilter SelectionTableFilter
class SelectionCompensationTableFilter(SelectionTableFilter): class SelectionCompensationTableFilter(SelectionTableFilter):
@ -58,10 +55,12 @@ class CheckboxCompensationTableFilter(CheckboxTableFilter):
""" """
if not value: if not value:
return queryset.filter( user_teams = self.user.shared_teams
result = queryset.filter(
Q(intervention__users__in=[self.user]) | # requesting user has access Q(intervention__users__in=[self.user]) | # requesting user has access
Q(intervention__teams__in=self.user.shared_teams) Q(intervention__teams__in=user_teams)
).distinct() ).distinct()
return result
else: else:
return queryset return queryset
@ -114,71 +113,3 @@ class CompensationTableFilter(AbstractTableFilter):
) )
# Overwrite final queryset as well # Overwrite final queryset as well
self.qs = self.checkbox_filter.qs self.qs = self.checkbox_filter.qs
class CheckboxEcoAccountTableFilter(CheckboxTableFilter):
sr = django_filters.BooleanFilter(
method='filter_only_show_unrecorded',
label=_("Show only unrecorded"),
label_suffix=_(""),
widget=forms.CheckboxInput(
attrs={
"class": "form-check-input",
}
)
)
def filter_only_show_unrecorded(self, queryset, name, value) -> QuerySet:
""" Filters queryset depending on value of 'show_recorded' setting
Args:
queryset ():
name ():
value ():
Returns:
"""
if value:
return queryset.filter(
recorded=None,
)
else:
return queryset
class SelectionEcoAccountTableFilter(ConservationOfficeTableFilterMixin):
""" Special selection table filter for eco accounts
EcoAccounts only need a selection filter for conservation office
"""
pass
class EcoAccountTableFilter(AbstractTableFilter):
""" TableFilter for eco accounts
"""
def __init__(self, user=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
qs = kwargs.get("queryset", None)
request_data = kwargs.get("data", None)
# Pipe the queryset through all needed filters
self.selection_filter = SelectionEcoAccountTableFilter(
data=request_data,
queryset=qs,
)
self.query_filter = QueryTableFilter(
data=request_data,
queryset=self.selection_filter.qs,
)
self.checkbox_filter = CheckboxEcoAccountTableFilter(
user=user,
data=request_data,
queryset=self.query_filter.qs,
)
# Overwrite the final queryset result
self.qs = self.checkbox_filter.qs

View File

@ -0,0 +1,82 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django import forms
from django.db.models import QuerySet
from django.utils.translation import gettext_lazy as _
import django_filters
from konova.filters.mixins.office import ConservationOfficeTableFilterMixin
from konova.filters.table_filters import AbstractTableFilter, CheckboxTableFilter, QueryTableFilter
class CheckboxEcoAccountTableFilter(CheckboxTableFilter):
sr = django_filters.BooleanFilter(
method='filter_only_show_unrecorded',
label=_("Show only unrecorded"),
label_suffix=_(""),
widget=forms.CheckboxInput(
attrs={
"class": "form-check-input",
}
)
)
def filter_only_show_unrecorded(self, queryset, name, value) -> QuerySet:
""" Filters queryset depending on value of 'show_recorded' setting
Args:
queryset ():
name ():
value ():
Returns:
"""
if value:
return queryset.filter(
recorded=None,
)
else:
return queryset
class SelectionEcoAccountTableFilter(ConservationOfficeTableFilterMixin):
""" Special selection table filter for eco accounts
EcoAccounts only need a selection filter for conservation office
"""
pass
class EcoAccountTableFilter(AbstractTableFilter):
""" TableFilter for eco accounts
"""
def __init__(self, user=None, *args, **kwargs):
super().__init__(*args, **kwargs)
self.user = user
qs = kwargs.get("queryset", None)
request_data = kwargs.get("data", None)
# Pipe the queryset through all needed filters
self.selection_filter = SelectionEcoAccountTableFilter(
data=request_data,
queryset=qs,
)
self.query_filter = QueryTableFilter(
data=request_data,
queryset=self.selection_filter.qs,
)
self.checkbox_filter = CheckboxEcoAccountTableFilter(
user=user,
data=request_data,
queryset=self.query_filter.qs,
)
# Overwrite the final queryset result
self.qs = self.checkbox_filter.qs

View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""

View File

@ -0,0 +1,240 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal import autocomplete
from django import forms
from django.db import transaction
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from compensation.forms.mixins import CEFCompensationFormMixin, CoherenceCompensationFormMixin, PikCompensationFormMixin
from compensation.models import Compensation
from intervention.inputs import GenerateInput
from intervention.models import Intervention
from konova.forms import BaseForm, SimpleGeomForm
from konova.utils.message_templates import COMPENSATION_ADDED_TEMPLATE, EDITED_GENERAL_DATA
from user.models import UserActionLogEntry, User
class AbstractCompensationForm(BaseForm):
""" Abstract form for compensations
Holds all important form fields, which are used in compensation and eco account forms
"""
identifier = forms.CharField(
label=_("Identifier"),
label_suffix="",
max_length=255,
help_text=_("Generated automatically - not editable"),
widget=GenerateInput(
attrs={
"class": "form-control",
"url": None, # Needs to be set in inheriting constructors
"readonly": True,
}
)
)
title = forms.CharField(
label=_("Title"),
label_suffix="",
help_text=_("An explanatory name"),
max_length=255,
widget=forms.TextInput(
attrs={
"placeholder": _("Compensation XY; Location ABC"),
"class": "form-control",
}
)
)
comment = forms.CharField(
label_suffix="",
label=_("Comment"),
required=False,
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
class Meta:
abstract = True
class NewCompensationForm(AbstractCompensationForm,
CEFCompensationFormMixin,
CoherenceCompensationFormMixin,
PikCompensationFormMixin):
""" Form for creating new compensations.
Can be initialized with an intervention id for preselecting the related intervention.
form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
...
The intervention id will not be resolved into the intervention ORM object but instead will be used to initialize
the related form field.
"""
intervention = forms.ModelChoiceField(
label=_("compensates intervention"),
label_suffix="",
help_text=_("Select the intervention for which this compensation compensates"),
queryset=Intervention.objects.filter(
deleted=None,
),
widget=autocomplete.ModelSelect2(
url="intervention:autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
"data-minimum-input-length": 3,
}
),
)
# Define a field order for a nicer layout instead of running with the inheritance result
field_order = [
"identifier",
"title",
"intervention",
"is_pik",
"is_cef",
"is_coherence_keeping",
"comment",
]
def __init__(self, *args, **kwargs):
intervention_id = kwargs.pop("intervention_id", None)
super().__init__(*args, **kwargs)
self.form_title = _("New compensation")
# If the compensation shall directly be initialized from an intervention, we need to fill in the intervention id
# and disable the form field.
# Furthermore the action_url needs to be set accordingly.
if intervention_id is not None:
self.initialize_form_field("intervention", intervention_id)
self.disable_form_field("intervention")
self.action_url = reverse("compensation:new", args=(intervention_id,))
self.cancel_redirect = reverse("intervention:detail", args=(intervention_id,))
else:
self.action_url = reverse("compensation:new")
self.cancel_redirect = reverse("compensation:index")
tmp = Compensation()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id")
def __create_comp(self, user):
""" Creates the compensation from form data
Args:
user (User): The performing user
Returns:
comp (Compensation): The compensation object
"""
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_created_action(user)
# Finally create main object
comp = Compensation.objects.create(
identifier=identifier,
title=title,
intervention=intervention,
created=action,
modified=action,
is_cef=is_cef,
is_coherence_keeping=is_coherence_keeping,
is_pik=is_pik,
comment=comment,
)
# Add the log entry to the main objects log list
comp.log.add(action)
return comp, action
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
comp, action = self.__create_comp(user)
comp.intervention.mark_as_edited(user, edit_comment=COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
# Process the geometry form
geometry = geom_form.save(action)
comp.geometry = geometry
comp.save()
return comp
class EditCompensationForm(NewCompensationForm):
""" Form for editing compensations
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit compensation")
self.action_url = reverse("compensation:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:detail", args=(self.instance.id,))
# Initialize form data
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"intervention": self.instance.intervention,
"is_cef": self.instance.is_cef,
"is_coherence_keeping": self.instance.is_coherence_keeping,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Fetch data from cleaned POST values
title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
self.instance.title = title
self.instance.intervention = intervention
self.instance.is_cef = is_cef
self.instance.is_coherence_keeping = is_coherence_keeping
self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
self.instance.log.add(action)
intervention.mark_as_edited(user, self.request, EDITED_GENERAL_DATA)
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action)
self.instance.geometry = geometry
self.instance.save()
return self.instance

View File

@ -0,0 +1,249 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django import forms
from django.db import transaction
from django.urls import reverse, reverse_lazy
from django.utils.translation import gettext_lazy as _
from compensation.forms.compensation import AbstractCompensationForm
from compensation.forms.mixins import CompensationResponsibleFormMixin, PikCompensationFormMixin
from compensation.models import EcoAccount
from intervention.models import Handler, Responsibility, Legal
from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm
from konova.utils import validators
from user.models import User, UserActionLogEntry
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
""" Form for creating eco accounts
Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin
"""
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Available Surface"),
label_suffix="",
required=False,
help_text=_("The amount that can be used for deductions"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
registration_date = forms.DateField(
label=_("Agreement date"),
label_suffix="",
help_text=_("When did the parties agree on this?"),
required=False,
validators=[validators.reasonable_date],
widget=forms.DateInput(
attrs={
"type": "date",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
field_order = [
"identifier",
"title",
"conservation_office",
"registration_date",
"surface",
"conservation_file_number",
"is_pik",
"handler_type",
"handler_detail",
"comment",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New Eco-Account")
self.action_url = reverse("compensation:acc:new")
self.cancel_redirect = reverse("compensation:acc:index")
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:acc:new-id")
self.fields["title"].widget.attrs["placeholder"] = _("Eco-Account XY; Location ABC")
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_created_action(user)
handler = Handler.objects.create(
type=handler_type,
detail=handler_detail,
)
responsible = Responsibility.objects.create(
handler=handler,
conservation_file_number=conservation_file_number,
conservation_office=conservation_office,
)
legal = Legal.objects.create(
registration_date=registration_date
)
# Finally create main object
acc = EcoAccount.objects.create(
identifier=identifier,
title=title,
responsible=responsible,
deductable_surface=surface,
created=action,
modified=action,
comment=comment,
is_pik=is_pik,
legal=legal
)
acc.share_with_user(user)
# Add the log entry to the main objects log list
acc.log.add(action)
# Process the geometry form
geometry = geom_form.save(action)
acc.geometry = geometry
acc.save()
acc.update_deductable_rest()
return acc
class EditEcoAccountForm(NewEcoAccountForm):
""" Form for editing eco accounts
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit Eco-Account")
self.action_url = reverse("compensation:acc:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:acc:detail", args=(self.instance.id,))
# Initialize form data
reg_date = self.instance.legal.registration_date
if reg_date is not None:
reg_date = reg_date.isoformat()
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"surface": self.instance.deductable_surface,
"handler_type": self.instance.responsible.handler.type,
"handler_detail": self.instance.responsible.handler.detail,
"registration_date": reg_date,
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def is_valid(self):
valid = super().is_valid()
deductable_surface = self.cleaned_data.get("surface") or 0.0
deduction_surface_sum = self.instance.get_deductions_surface()
if deductable_surface < deduction_surface_sum:
self.add_error(
"surface",
_("{}m² have been deducted from this eco account so far. The given value of {} would be too low.").format(
deduction_surface_sum,
deductable_surface
)
)
valid &= False
return valid
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None)
is_pik = self.cleaned_data.get("is_pik", None)
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Update responsible data
self.instance.responsible.handler.type = handler_type
self.instance.responsible.handler.detail = handler_detail
self.instance.responsible.handler.save()
self.instance.responsible.conservation_office = conservation_office
self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save()
# Update legal data
self.instance.legal.registration_date = registration_date
self.instance.legal.save()
# Update main oject data
self.instance.title = title
self.instance.deductable_surface = surface
self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
# Add the log entry to the main objects log list
self.instance.log.add(action)
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action)
self.instance.geometry = geometry
self.instance.save()
self.instance.update_deductable_rest()
return self.instance
class RemoveEcoAccountModalForm(RemoveModalForm):
def is_valid(self):
super_valid = super().is_valid()
has_deductions = self.instance.deductions.exists()
if has_deductions:
self.add_error(
"confirm",
_("The account can not be removed, since there are still deductions.")
)
return super_valid and not has_deductions

View File

@ -1,539 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.12.20
"""
from dal import autocomplete
from user.models import User
from django.db import transaction
from django.urls import reverse_lazy, reverse
from django.utils.translation import gettext_lazy as _
from django import forms
from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
from compensation.models import Compensation, EcoAccount
from intervention.inputs import GenerateInput
from intervention.models import Intervention, Responsibility, Legal, Handler
from konova.forms import BaseForm, SimpleGeomForm
from konova.utils.message_templates import EDITED_GENERAL_DATA, COMPENSATION_ADDED_TEMPLATE
from user.models import UserActionLogEntry
class AbstractCompensationForm(BaseForm):
""" Abstract form for compensations
Holds all important form fields, which are used in compensation and eco account forms
"""
identifier = forms.CharField(
label=_("Identifier"),
label_suffix="",
max_length=255,
help_text=_("Generated automatically"),
widget=GenerateInput(
attrs={
"class": "form-control",
"url": None, # Needs to be set in inheriting constructors
}
)
)
title = forms.CharField(
label=_("Title"),
label_suffix="",
help_text=_("An explanatory name"),
max_length=255,
widget=forms.TextInput(
attrs={
"placeholder": _("Compensation XY; Location ABC"),
"class": "form-control",
}
)
)
comment = forms.CharField(
label_suffix="",
label=_("Comment"),
required=False,
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
class Meta:
abstract = True
class CompensationResponsibleFormMixin(forms.Form):
""" Encapsulates form fields used in different compensation related models like EcoAccount or EMA
"""
conservation_office = forms.ModelChoiceField(
label=_("Conservation office"),
label_suffix="",
help_text=_("Select the responsible office"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-conservation-office-autocomplete",
attrs={
"data-placeholder": _("Click for selection")
}
),
)
conservation_file_number = forms.CharField(
label=_("Conservation office file number"),
label_suffix="",
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
"placeholder": _("ETS-123/ABC.456"),
"class": "form-control",
}
)
)
handler_type = forms.ModelChoiceField(
label=_("Eco-Account handler type"),
label_suffix="",
help_text=_("What type of handler is responsible for the ecoaccount?"),
required=False,
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_HANDLER_ID],
),
widget=autocomplete.ModelSelect2(
url="codes-handler-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
}
),
)
handler_detail = forms.CharField(
label=_("Eco-Account handler detail"),
label_suffix="",
max_length=255,
required=False,
help_text=_("Detail input on the handler"),
widget=forms.TextInput(
attrs={
"placeholder": _("Company Mustermann"),
"class": "form-control",
}
)
)
class CEFCompensationFormMixin(forms.Form):
""" A form mixin, providing CEF compensation field
"""
is_cef = forms.BooleanField(
label_suffix="",
label=_("Is CEF"),
help_text=_("Optionally: Whether this compensation is a CEF compensation?"),
required=False,
widget=forms.CheckboxInput()
)
class CoherenceCompensationFormMixin(forms.Form):
""" A form mixin, providing coherence compensation field
"""
is_coherence_keeping = forms.BooleanField(
label_suffix="",
label=_("Is coherence keeping"),
help_text=_("Optionally: Whether this compensation is a coherence keeping compensation?"),
required=False,
widget=forms.CheckboxInput()
)
class PikCompensationFormMixin(forms.Form):
""" A form mixin, providing PIK compensation field
"""
is_pik = forms.BooleanField(
label_suffix="",
label=_("Is PIK"),
help_text=_("Optionally: Whether this compensation is a compensation integrated in production?"),
required=False,
widget=forms.CheckboxInput()
)
class NewCompensationForm(AbstractCompensationForm,
CEFCompensationFormMixin,
CoherenceCompensationFormMixin,
PikCompensationFormMixin):
""" Form for creating new compensations.
Can be initialized with an intervention id for preselecting the related intervention.
form = NewCompensationForm(request.POST or None, intervention_id=intervention_id)
...
The intervention id will not be resolved into the intervention ORM object but instead will be used to initialize
the related form field.
"""
intervention = forms.ModelChoiceField(
label=_("compensates intervention"),
label_suffix="",
help_text=_("Select the intervention for which this compensation compensates"),
queryset=Intervention.objects.filter(
deleted=None,
),
widget=autocomplete.ModelSelect2(
url="interventions-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
"data-minimum-input-length": 3,
}
),
)
# Define a field order for a nicer layout instead of running with the inheritance result
field_order = [
"identifier",
"title",
"intervention",
"is_pik",
"is_cef",
"is_coherence_keeping",
"comment",
]
def __init__(self, *args, **kwargs):
intervention_id = kwargs.pop("intervention_id", None)
super().__init__(*args, **kwargs)
self.form_title = _("New compensation")
# If the compensation shall directly be initialized from an intervention, we need to fill in the intervention id
# and disable the form field.
# Furthermore the action_url needs to be set accordingly.
if intervention_id is not None:
self.initialize_form_field("intervention", intervention_id)
self.disable_form_field("intervention")
self.action_url = reverse("compensation:new", args=(intervention_id,))
self.cancel_redirect = reverse("intervention:detail", args=(intervention_id,))
else:
self.action_url = reverse("compensation:new")
self.cancel_redirect = reverse("compensation:index")
tmp = Compensation()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:new-id")
def __create_comp(self, user, geom_form) -> Compensation:
""" Creates the compensation from form data
Args:
user (User): The performing user
geom_form (SimpleGeomForm): The geometry form
Returns:
comp (Compensation): The compensation object
"""
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object
comp = Compensation.objects.create(
identifier=identifier,
title=title,
intervention=intervention,
created=action,
is_cef=is_cef,
is_coherence_keeping=is_coherence_keeping,
is_pik=is_pik,
geometry=geometry,
comment=comment,
)
# Add the log entry to the main objects log list
comp.log.add(action)
return comp
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
comp = self.__create_comp(user, geom_form)
comp.intervention.mark_as_edited(user, edit_comment=COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
return comp
class EditCompensationForm(NewCompensationForm):
""" Form for editing compensations
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit compensation")
self.action_url = reverse("compensation:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:detail", args=(self.instance.id,))
# Initialize form data
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"intervention": self.instance.intervention,
"is_cef": self.instance.is_cef,
"is_coherence_keeping": self.instance.is_coherence_keeping,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None)
is_coherence_keeping = self.cleaned_data.get("is_coherence_keeping", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Finally create main object
self.instance.identifier = identifier
self.instance.title = title
self.instance.intervention = intervention
self.instance.geometry = geometry
self.instance.is_cef = is_cef
self.instance.is_coherence_keeping = is_coherence_keeping
self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
self.instance.log.add(action)
intervention.mark_as_edited(user, self.request, EDITED_GENERAL_DATA)
return self.instance
class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMixin, PikCompensationFormMixin):
""" Form for creating eco accounts
Inherits from basic AbstractCompensationForm and further form fields from CompensationResponsibleFormMixin
"""
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Available Surface"),
label_suffix="",
required=False,
help_text=_("The amount that can be used for deductions"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
registration_date = forms.DateField(
label=_("Agreement date"),
label_suffix="",
help_text=_("When did the parties agree on this?"),
required=False,
widget=forms.DateInput(
attrs={
"type": "date",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
field_order = [
"identifier",
"title",
"conservation_office",
"registration_date",
"surface",
"conservation_file_number",
"is_pik",
"handler_type",
"handler_detail",
"comment",
]
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New Eco-Account")
self.action_url = reverse("compensation:acc:new")
self.cancel_redirect = reverse("compensation:acc:index")
tmp = EcoAccount()
identifier = tmp.generate_new_identifier()
self.initialize_form_field("identifier", identifier)
self.fields["identifier"].widget.attrs["url"] = reverse_lazy("compensation:acc:new-id")
self.fields["title"].widget.attrs["placeholder"] = _("Eco-Account XY; Location ABC")
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None)
# Create log entry
action = UserActionLogEntry.get_created_action(user)
# Process the geometry form
geometry = geom_form.save(action)
handler = Handler.objects.create(
type=handler_type,
detail=handler_detail,
)
responsible = Responsibility.objects.create(
handler=handler,
conservation_file_number=conservation_file_number,
conservation_office=conservation_office,
)
legal = Legal.objects.create(
registration_date=registration_date
)
# Finally create main object
acc = EcoAccount.objects.create(
identifier=identifier,
title=title,
responsible=responsible,
deductable_surface=surface,
created=action,
geometry=geometry,
comment=comment,
is_pik=is_pik,
legal=legal
)
acc.share_with_user(user)
# Add the log entry to the main objects log list
acc.log.add(action)
return acc
class EditEcoAccountForm(NewEcoAccountForm):
""" Form for editing eco accounts
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("Edit Eco-Account")
self.action_url = reverse("compensation:acc:edit", args=(self.instance.id,))
self.cancel_redirect = reverse("compensation:acc:detail", args=(self.instance.id,))
# Initialize form data
reg_date = self.instance.legal.registration_date
if reg_date is not None:
reg_date = reg_date.isoformat()
form_data = {
"identifier": self.instance.identifier,
"title": self.instance.title,
"surface": self.instance.deductable_surface,
"handler_type": self.instance.responsible.handler.type,
"handler_detail": self.instance.responsible.handler.detail,
"registration_date": reg_date,
"conservation_office": self.instance.responsible.conservation_office,
"conservation_file_number": self.instance.responsible.conservation_file_number,
"is_pik": self.instance.is_pik,
"comment": self.instance.comment,
}
disabled_fields = []
self.load_initial_data(
form_data,
disabled_fields
)
def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic():
# Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None)
surface = self.cleaned_data.get("surface", None)
conservation_office = self.cleaned_data.get("conservation_office", None)
conservation_file_number = self.cleaned_data.get("conservation_file_number", None)
comment = self.cleaned_data.get("comment", None)
is_pik = self.cleaned_data.get("is_pik", None)
# Create log entry
action = UserActionLogEntry.get_edited_action(user)
# Process the geometry form
geometry = geom_form.save(action)
# Update responsible data
self.instance.responsible.handler.type = handler_type
self.instance.responsible.handler.detail = handler_detail
self.instance.responsible.handler.save()
self.instance.responsible.conservation_office = conservation_office
self.instance.responsible.conservation_file_number = conservation_file_number
self.instance.responsible.save()
# Update legal data
self.instance.legal.registration_date = registration_date
self.instance.legal.save()
# Update main oject data
self.instance.identifier = identifier
self.instance.title = title
self.instance.deductable_surface = surface
self.instance.geometry = geometry
self.instance.comment = comment
self.instance.is_pik = is_pik
self.instance.modified = action
self.instance.save()
# Add the log entry to the main objects log list
self.instance.log.add(action)
return self.instance

View File

@ -0,0 +1,117 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal import autocomplete
from django import forms
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_CONSERVATION_OFFICE_ID, CODELIST_HANDLER_ID
class CompensationResponsibleFormMixin(forms.Form):
""" Encapsulates form fields used in different compensation related models like EcoAccount or EMA
"""
conservation_office = forms.ModelChoiceField(
label=_("Conservation office"),
label_suffix="",
help_text=_("Select the responsible office"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_CONSERVATION_OFFICE_ID],
),
widget=autocomplete.ModelSelect2(
url="codelist:conservation-office-autocomplete",
attrs={
"data-placeholder": _("Click for selection")
}
),
)
conservation_file_number = forms.CharField(
label=_("Conservation office file number"),
label_suffix="",
max_length=255,
required=False,
widget=forms.TextInput(
attrs={
"placeholder": _("ETS-123/ABC.456"),
"class": "form-control",
}
)
)
handler_type = forms.ModelChoiceField(
label=_("Eco-Account handler type"),
label_suffix="",
help_text=_("What type of handler is responsible for the ecoaccount?"),
required=False,
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_HANDLER_ID],
),
widget=autocomplete.ModelSelect2(
url="codelist:handler-autocomplete",
attrs={
"data-placeholder": _("Click for selection"),
}
),
)
handler_detail = forms.CharField(
label=_("Eco-Account handler detail"),
label_suffix="",
max_length=255,
required=False,
help_text=_("Detail input on the handler"),
widget=forms.TextInput(
attrs={
"placeholder": _("Company Mustermann"),
"class": "form-control",
}
)
)
class CEFCompensationFormMixin(forms.Form):
""" A form mixin, providing CEF compensation field
"""
is_cef = forms.BooleanField(
label_suffix="",
label=_("Is CEF"),
help_text=_("Optionally: Whether this compensation is a CEF compensation?"),
required=False,
widget=forms.CheckboxInput()
)
class CoherenceCompensationFormMixin(forms.Form):
""" A form mixin, providing coherence compensation field
"""
is_coherence_keeping = forms.BooleanField(
label_suffix="",
label=_("Is coherence keeping"),
help_text=_("Optionally: Whether this compensation is a coherence keeping compensation?"),
required=False,
widget=forms.CheckboxInput()
)
class PikCompensationFormMixin(forms.Form):
""" A form mixin, providing PIK compensation field
"""
is_pik = forms.BooleanField(
label_suffix="",
label=_("Is PIK"),
help_text=_("Optionally: Whether this compensation is a compensation integrated in production?"),
required=False,
widget=forms.CheckboxInput()
)

View File

@ -1,534 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.10.21
"""
from bootstrap_modal_forms.utils import is_ajax
from dal import autocomplete
from django import forms
from django.contrib import messages
from django.http import HttpRequest, HttpResponseRedirect
from django.shortcuts import render
from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_EXTRA_CODES_ID, \
CODELIST_COMPENSATION_ACTION_DETAIL_ID
from compensation.models import CompensationDocument, EcoAccountDocument
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple, \
CompensationStateTreeRadioSelect
from konova.contexts import BaseContext
from konova.forms import BaseModalForm, NewDocumentModalForm, RemoveModalForm
from konova.models import DeadlineType
from konova.utils.message_templates import FORM_INVALID, ADDED_COMPENSATION_STATE, \
ADDED_COMPENSATION_ACTION, PAYMENT_EDITED, COMPENSATION_STATE_EDITED, COMPENSATION_ACTION_EDITED, DEADLINE_EDITED
class NewPaymentForm(BaseModalForm):
""" Form handling payment related input
"""
amount = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_con("money", "Amount"), # contextual translation
label_suffix=_(""),
help_text=_("in Euro"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
)
due = forms.DateField(
label=_("Due on"),
label_suffix=_(""),
required=False,
help_text=_("Due on which date"),
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
max_length=200,
required=False,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.intervention = self.instance
self.form_title = _("Payment")
self.form_caption = _("Add a payment for intervention '{}'").format(self.intervention.title)
def is_valid(self):
"""
Checks on form validity.
For this form we need to make sure that a date or a comment is set.
If both are missing, the user needs to enter at least an explanation why
there is no date to be entered.
Returns:
is_valid (bool): True if valid, False otherwise
"""
super_valid = super().is_valid()
date = self.cleaned_data["due"]
comment = self.cleaned_data["comment"] or None
if not date and not comment:
# At least one needs to be set!
self.add_error(
"comment",
_("If there is no date you can enter, please explain why.")
)
return False
return super_valid
def save(self):
pay = self.instance.add_payment(self)
return pay
class EditPaymentModalForm(NewPaymentForm):
""" Form handling edit for Payment
"""
payment = None
def __init__(self, *args, **kwargs):
self.payment = kwargs.pop("payment", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit payment")
form_date = {
"amount": self.payment.amount,
"due": str(self.payment.due_on),
"comment": self.payment.comment,
}
self.load_initial_data(form_date, disabled_fields=[])
def save(self):
payment = self.payment
payment.amount = self.cleaned_data.get("amount", None)
payment.due_on = self.cleaned_data.get("due", None)
payment.comment = self.cleaned_data.get("comment", None)
payment.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_EDITED)
self.instance.send_data_to_egon()
return payment
class RemovePaymentModalForm(RemoveModalForm):
""" Removing modal form for Payment
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
payment = None
def __init__(self, *args, **kwargs):
payment = kwargs.pop("payment", None)
self.payment = payment
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_payment(self)
class NewStateModalForm(BaseModalForm):
""" Form handling state related input
Compensation states refer to 'before' and 'after' states of a compensated surface. Basically it means:
What has been on this area before changes/compensations have been applied and what will be the result ('after')?
"""
biotope_type = forms.ChoiceField(
label=_("Biotope Type"),
label_suffix="",
required=True,
help_text=_("Select the biotope type"),
widget=CompensationStateTreeRadioSelect(),
)
biotope_extra = forms.ModelMultipleChoiceField(
label=_("Biotope additional type"),
label_suffix="",
required=False,
help_text=_("Select an additional biotope type"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codes-biotope-extra-type-autocomplete",
attrs={
"data-placeholder": _("Biotope additional type"),
}
),
)
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Surface"),
label_suffix="",
required=True,
help_text=_("in m²"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New state")
self.form_caption = _("Insert data for the new state")
choices = KonovaCode.objects.filter(
code_lists__in=[CODELIST_BIOTOPES_ID],
is_archived=False,
is_leaf=True,
).values_list("id", flat=True)
choices = [
(choice, choice)
for choice in choices
]
self.fields["biotope_type"].choices = choices
def save(self, is_before_state: bool = False):
state = self.instance.add_state(self, is_before_state)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_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(NewStateModalForm):
state = None
def __init__(self, *args, **kwargs):
self.state = kwargs.pop("state", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit state")
biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None
form_data = {
"biotope_type": biotope_type_id,
"biotope_extra": self.state.biotope_type_details.all(),
"surface": self.state.surface,
}
self.load_initial_data(form_data)
def save(self, is_before_state: bool = False):
state = self.state
biotope_type_id = self.cleaned_data.get("biotope_type", None)
state.biotope_type = KonovaCode.objects.get(id=biotope_type_id)
state.biotope_type_details.set(self.cleaned_data.get("biotope_extra", []))
state.surface = self.cleaned_data.get("surface", None)
state.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_STATE_EDITED)
return state
class RemoveCompensationStateModalForm(RemoveModalForm):
""" Removing modal form for CompensationState
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
state = None
def __init__(self, *args, **kwargs):
state = kwargs.pop("state", None)
self.state = state
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_state(self)
class RemoveCompensationActionModalForm(RemoveModalForm):
""" Removing modal form for CompensationAction
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
action = None
def __init__(self, *args, **kwargs):
action = kwargs.pop("action", None)
self.action = action
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_action(self)
class NewDeadlineModalForm(BaseModalForm):
""" Form handling deadline related input
"""
type = forms.ChoiceField(
label=_("Deadline Type"),
label_suffix="",
required=True,
help_text=_("Select the deadline type"),
choices=DeadlineType.choices,
widget=forms.Select(
attrs={
"class": "form-control"
}
)
)
date = forms.DateField(
label=_("Date"),
label_suffix="",
required=True,
help_text=_("Select date"),
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
required=False,
max_length=200,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
widget=forms.Textarea(
attrs={
"cols": 30,
"rows": 5,
"class": "form-control",
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New deadline")
self.form_caption = _("Insert data for the new deadline")
def save(self):
deadline = self.instance.add_deadline(self)
return deadline
class EditDeadlineModalForm(NewDeadlineModalForm):
deadline = None
def __init__(self, *args, **kwargs):
self.deadline = kwargs.pop("deadline", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit deadline")
form_data = {
"type": self.deadline.type,
"date": str(self.deadline.date),
"comment": self.deadline.comment,
}
self.load_initial_data(form_data)
def save(self):
deadline = self.deadline
deadline.type = self.cleaned_data.get("type", None)
deadline.date = self.cleaned_data.get("date", None)
deadline.comment = self.cleaned_data.get("comment", None)
deadline.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=DEADLINE_EDITED)
return deadline
class NewActionModalForm(BaseModalForm):
""" Form handling action related input
Compensation actions are the actions performed on the area, which shall be compensated. Actions will change the
surface of the area, the biotopes, and have an environmental impact. With actions the before-after states can change
(not in the process logic in Konova, but in the real world).
"""
from compensation.models import UnitChoices
action_type = forms.MultipleChoiceField(
label=_("Action Type"),
label_suffix="",
required=True,
help_text=_("An action can consist of multiple different action types. All the selected action types are expected to be performed according to the amount and unit below on this form."),
choices=[],
widget=CompensationActionTreeCheckboxSelectMultiple(),
)
action_type_details = forms.ModelMultipleChoiceField(
label=_("Action Type detail"),
label_suffix="",
required=False,
help_text=_("Select the action type detail"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_COMPENSATION_ACTION_DETAIL_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codes-compensation-action-detail-autocomplete",
attrs={
"data-placeholder": _("Action Type detail"),
}
),
)
unit = forms.ChoiceField(
label=_("Unit"),
label_suffix="",
required=True,
help_text=_("Select the unit"),
choices=UnitChoices.choices,
widget=forms.Select(
attrs={
"class": "form-control"
}
)
)
amount = forms.DecimalField(
label=_("Amount"),
label_suffix="",
required=True,
help_text=_("Insert the amount"),
decimal_places=2,
min_value=0.00,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
)
comment = forms.CharField(
required=False,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control",
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New action")
self.form_caption = _("Insert data for the new action")
choices =KonovaCode.objects.filter(
code_lists__in=[CODELIST_COMPENSATION_ACTION_ID],
is_archived=False,
is_leaf=True,
).values_list("id", flat=True)
choices = [
(choice, choice)
for choice in choices
]
self.fields["action_type"].choices = choices
def save(self):
action = self.instance.add_action(self)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_ACTION)
return action
class EditCompensationActionModalForm(NewActionModalForm):
action = None
def __init__(self, *args, **kwargs):
self.action = kwargs.pop("action", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit action")
form_data = {
"action_type": list(self.action.action_type.values_list("id", flat=True)),
"action_type_details": self.action.action_type_details.all(),
"amount": self.action.amount,
"unit": self.action.unit,
"comment": self.action.comment,
}
self.load_initial_data(form_data)
def save(self):
action = self.action
action.action_type.set(self.cleaned_data.get("action_type", []))
action.action_type_details.set(self.cleaned_data.get("action_type_details", []))
action.amount = self.cleaned_data.get("amount", None)
action.unit = self.cleaned_data.get("unit", None)
action.comment = self.cleaned_data.get("comment", None)
action.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_ACTION_EDITED)
return action
class NewCompensationDocumentModalForm(NewDocumentModalForm):
document_model = CompensationDocument
class NewEcoAccountDocumentModalForm(NewDocumentModalForm):
document_model = EcoAccountDocument

View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""

View File

@ -0,0 +1,155 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from dal import autocomplete
from django import forms
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCode
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_COMPENSATION_ACTION_DETAIL_ID
from intervention.inputs import CompensationActionTreeCheckboxSelectMultiple
from konova.forms.modals import BaseModalForm, RemoveModalForm
from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, ADDED_COMPENSATION_ACTION
class NewCompensationActionModalForm(BaseModalForm):
""" Form handling action related input
Compensation actions are the actions performed on the area, which shall be compensated. Actions will change the
surface of the area, the biotopes, and have an environmental impact. With actions the before-after states can change
(not in the process logic in Konova, but in the real world).
"""
from compensation.models import UnitChoices
action_type = forms.MultipleChoiceField(
label=_("Action Type"),
label_suffix="",
required=True,
help_text=_("An action can consist of multiple different action types. All the selected action types are expected to be performed according to the amount and unit below on this form."),
choices=[],
widget=CompensationActionTreeCheckboxSelectMultiple(),
)
action_type_details = forms.ModelMultipleChoiceField(
label=_("Action Type detail"),
label_suffix="",
required=False,
help_text=_("Select the action type detail"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_COMPENSATION_ACTION_DETAIL_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codelist:compensation-action-detail-autocomplete",
attrs={
"data-placeholder": _("Action Type detail"),
}
),
)
unit = forms.ChoiceField(
label=_("Unit"),
label_suffix="",
required=True,
help_text=_("Select the unit"),
choices=UnitChoices.choices,
widget=forms.Select(
attrs={
"class": "form-control"
}
)
)
amount = forms.DecimalField(
label=_("Amount"),
label_suffix="",
required=True,
help_text=_("Insert the amount"),
decimal_places=2,
min_value=0.00,
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
)
comment = forms.CharField(
required=False,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment"),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control",
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New action")
self.form_caption = _("Insert data for the new action")
choices = KonovaCode.objects.filter(
code_lists__in=[CODELIST_COMPENSATION_ACTION_ID],
is_archived=False,
is_leaf=True,
).values_list("id", flat=True)
choices = [
(choice, choice)
for choice in choices
]
self.fields["action_type"].choices = choices
def save(self):
action = self.instance.add_action(self)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_ACTION)
return action
class EditCompensationActionModalForm(NewCompensationActionModalForm):
action = None
def __init__(self, *args, **kwargs):
self.action = kwargs.pop("action", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit action")
form_data = {
"action_type": list(self.action.action_type.values_list("id", flat=True)),
"action_type_details": self.action.action_type_details.all(),
"amount": self.action.amount,
"unit": self.action.unit,
"comment": self.action.comment,
}
self.load_initial_data(form_data)
def save(self):
action = self.action
action.action_type.set(self.cleaned_data.get("action_type", []))
action.action_type_details.set(self.cleaned_data.get("action_type_details", []))
action.amount = self.cleaned_data.get("amount", None)
action.unit = self.cleaned_data.get("unit", None)
action.comment = self.cleaned_data.get("comment", None)
action.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_ACTION_EDITED)
return action
class RemoveCompensationActionModalForm(RemoveModalForm):
""" Removing modal form for CompensationAction
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
action = None
def __init__(self, *args, **kwargs):
action = kwargs.pop("action", None)
self.action = action
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_action(self)

View File

@ -0,0 +1,110 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django import forms
from django.utils.translation import gettext_lazy as _
from konova.forms.modals import BaseModalForm
from konova.models import DeadlineType
from konova.utils import validators
from konova.utils.message_templates import DEADLINE_EDITED
class NewDeadlineModalForm(BaseModalForm):
""" Form handling deadline related input
"""
type = forms.ChoiceField(
label=_("Deadline Type"),
label_suffix="",
required=True,
help_text=_("Select the deadline type"),
choices=DeadlineType.choices,
widget=forms.Select(
attrs={
"class": "form-control"
}
)
)
date = forms.DateField(
label=_("Date"),
label_suffix="",
required=True,
help_text=_("Select date"),
validators=[validators.reasonable_date],
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
required=False,
max_length=200,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
widget=forms.Textarea(
attrs={
"cols": 30,
"rows": 5,
"class": "form-control",
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New deadline")
self.form_caption = _("Insert data for the new deadline")
def is_valid(self):
valid = super().is_valid()
deadline_type = self.cleaned_data.get("type")
comment = self.cleaned_data.get("comment") or None
other_deadline_without_comment = deadline_type == DeadlineType.OTHER and comment is None
if other_deadline_without_comment:
self.add_error(
"comment",
_("Please explain this 'other' type of deadline.")
)
valid &= False
return valid
def save(self):
deadline = self.instance.add_deadline(self)
return deadline
class EditDeadlineModalForm(NewDeadlineModalForm):
deadline = None
def __init__(self, *args, **kwargs):
self.deadline = kwargs.pop("deadline", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit deadline")
form_data = {
"type": self.deadline.type,
"date": str(self.deadline.date),
"comment": self.deadline.comment,
}
self.load_initial_data(form_data)
def save(self):
deadline = self.deadline
deadline.type = self.cleaned_data.get("type", None)
deadline.date = self.cleaned_data.get("date", None)
deadline.comment = self.cleaned_data.get("comment", None)
deadline.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=DEADLINE_EDITED)
return deadline

View File

@ -0,0 +1,17 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from compensation.models import CompensationDocument, EcoAccountDocument
from konova.forms.modals import NewDocumentModalForm
class NewCompensationDocumentModalForm(NewDocumentModalForm):
document_model = CompensationDocument
class NewEcoAccountDocumentModalForm(NewDocumentModalForm):
document_model = EcoAccountDocument

View File

@ -0,0 +1,141 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django import forms
from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _
from konova.forms.modals import RemoveModalForm, BaseModalForm
from konova.utils import validators
from konova.utils.message_templates import PAYMENT_EDITED
class NewPaymentForm(BaseModalForm):
""" Form handling payment related input
"""
amount = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_con("money", "Amount"), # contextual translation
label_suffix=_(""),
help_text=_("in Euro"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00",
}
)
)
due = forms.DateField(
label=_("Due on"),
label_suffix=_(""),
required=False,
validators=[validators.reasonable_date],
help_text=_("Due on which date"),
widget=forms.DateInput(
attrs={
"type": "date",
"data-provide": "datepicker",
"class": "form-control",
},
format="%d.%m.%Y"
)
)
comment = forms.CharField(
max_length=200,
required=False,
label=_("Comment"),
label_suffix=_(""),
help_text=_("Additional comment, maximum {} letters").format(200),
widget=forms.Textarea(
attrs={
"rows": 5,
"class": "form-control"
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.intervention = self.instance
self.form_title = _("Payment")
self.form_caption = _("Add a payment for intervention '{}'").format(self.intervention.title)
def is_valid(self):
"""
Checks on form validity.
For this form we need to make sure that a date or a comment is set.
If both are missing, the user needs to enter at least an explanation why
there is no date to be entered.
Returns:
is_valid (bool): True if valid, False otherwise
"""
super_valid = super().is_valid()
if not super_valid:
return super_valid
date = self.cleaned_data.get("due", None)
comment = self.cleaned_data.get("comment", None)
if not date and not comment:
# At least one needs to be set!
self.add_error(
"comment",
_("If there is no date you can enter, please explain why.")
)
return False
return super_valid
def save(self):
pay = self.instance.add_payment(self)
return pay
class EditPaymentModalForm(NewPaymentForm):
""" Form handling edit for Payment
"""
payment = None
def __init__(self, *args, **kwargs):
self.payment = kwargs.pop("payment", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit payment")
form_date = {
"amount": self.payment.amount,
"due": str(self.payment.due_on),
"comment": self.payment.comment,
}
self.load_initial_data(form_date, disabled_fields=[])
def save(self):
payment = self.payment
payment.amount = self.cleaned_data.get("amount", None)
payment.due_on = self.cleaned_data.get("due", None)
payment.comment = self.cleaned_data.get("comment", None)
payment.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=PAYMENT_EDITED)
self.instance.send_data_to_egon()
return payment
class RemovePaymentModalForm(RemoveModalForm):
""" Removing modal form for Payment
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
payment = None
def __init__(self, *args, **kwargs):
payment = kwargs.pop("payment", None)
self.payment = payment
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_payment(self)

View File

@ -0,0 +1,180 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from bootstrap_modal_forms.mixins import is_ajax
from dal import autocomplete
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 codelist.models import KonovaCode
from codelist.settings import CODELIST_BIOTOPES_ID, \
CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID
from intervention.inputs import CompensationStateTreeRadioSelect
from konova.contexts import BaseContext
from konova.forms.modals import RemoveModalForm, BaseModalForm
from konova.utils.message_templates import COMPENSATION_STATE_EDITED, FORM_INVALID, ADDED_COMPENSATION_STATE
class NewCompensationStateModalForm(BaseModalForm):
""" Form handling state related input
Compensation states refer to 'before' and 'after' states of a compensated surface. Basically it means:
What has been on this area before changes/compensations have been applied and what will be the result ('after')?
"""
biotope_type = forms.ChoiceField(
label=_("Biotope Type"),
label_suffix="",
required=True,
help_text=_("Select the biotope type"),
widget=CompensationStateTreeRadioSelect(),
)
biotope_extra = forms.ModelMultipleChoiceField(
label=_("Biotope additional type"),
label_suffix="",
required=False,
help_text=_("Select an additional biotope type"),
queryset=KonovaCode.objects.filter(
is_archived=False,
is_leaf=True,
code_lists__in=[CODELIST_BIOTOPES_EXTRA_CODES_FULL_ID],
),
widget=autocomplete.ModelSelect2Multiple(
url="codelist:biotope-extra-type-autocomplete",
attrs={
"data-placeholder": _("Biotope additional type"),
}
),
)
surface = forms.DecimalField(
min_value=0.00,
decimal_places=2,
label=_("Surface"),
label_suffix="",
required=True,
help_text=_("in m²"),
widget=forms.NumberInput(
attrs={
"class": "form-control",
"placeholder": "0,00"
}
)
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.form_title = _("New state")
self.form_caption = _("Insert data for the new state")
choices = KonovaCode.objects.filter(
code_lists__in=[CODELIST_BIOTOPES_ID],
is_archived=False,
is_leaf=True,
).values_list("id", flat=True)
choices = [
(choice, choice)
for choice in choices
]
self.fields["biotope_type"].choices = choices
def save(self, is_before_state: bool = False):
state = self.instance.add_state(self, is_before_state)
self.instance.mark_as_edited(self.user, self.request, ADDED_COMPENSATION_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):
state = None
def __init__(self, *args, **kwargs):
self.state = kwargs.pop("state", None)
super().__init__(*args, **kwargs)
self.form_title = _("Edit state")
biotope_type_id = self.state.biotope_type.id if self.state.biotope_type else None
form_data = {
"biotope_type": biotope_type_id,
"biotope_extra": self.state.biotope_type_details.all(),
"surface": self.state.surface,
}
self.load_initial_data(form_data)
def save(self, is_before_state: bool = False):
state = self.state
biotope_type_id = self.cleaned_data.get("biotope_type", None)
state.biotope_type = KonovaCode.objects.get(id=biotope_type_id)
state.biotope_type_details.set(self.cleaned_data.get("biotope_extra", []))
state.surface = self.cleaned_data.get("surface", None)
state.save()
self.instance.mark_as_edited(self.user, self.request, edit_comment=COMPENSATION_STATE_EDITED)
return state
class RemoveCompensationStateModalForm(RemoveModalForm):
""" Removing modal form for CompensationState
Can be used for anything, where removing shall be confirmed by the user a second time.
"""
state = None
def __init__(self, *args, **kwargs):
state = kwargs.pop("state", None)
self.state = state
super().__init__(*args, **kwargs)
def save(self):
self.instance.remove_state(self)

View File

@ -0,0 +1,24 @@
# Generated by Django 3.1.3 on 2022-08-15 06:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0014_resubmission'),
('compensation', '0007_auto_20220531_1245'),
]
operations = [
migrations.AddField(
model_name='compensation',
name='resubmission',
field=models.ManyToManyField(blank=True, null=True, related_name='_compensation_resubmission_+', to='konova.Resubmission'),
),
migrations.AddField(
model_name='ecoaccount',
name='resubmission',
field=models.ManyToManyField(blank=True, null=True, related_name='_ecoaccount_resubmission_+', to='konova.Resubmission'),
),
]

View File

@ -0,0 +1,32 @@
# Generated by Django 3.1.3 on 2022-08-15 06:03
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0014_resubmission'),
('compensation', '0008_auto_20220815_0803'),
]
operations = [
migrations.RemoveField(
model_name='compensation',
name='resubmission',
),
migrations.RemoveField(
model_name='ecoaccount',
name='resubmission',
),
migrations.AddField(
model_name='compensation',
name='resubmissions',
field=models.ManyToManyField(blank=True, null=True, related_name='_compensation_resubmissions_+', to='konova.Resubmission'),
),
migrations.AddField(
model_name='ecoaccount',
name='resubmissions',
field=models.ManyToManyField(blank=True, null=True, related_name='_ecoaccount_resubmissions_+', to='konova.Resubmission'),
),
]

View File

@ -0,0 +1,24 @@
# Generated by Django 3.1.3 on 2022-08-15 08:30
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0014_resubmission'),
('compensation', '0009_auto_20220815_0803'),
]
operations = [
migrations.AlterField(
model_name='compensation',
name='resubmissions',
field=models.ManyToManyField(blank=True, related_name='_compensation_resubmissions_+', to='konova.Resubmission'),
),
migrations.AlterField(
model_name='ecoaccount',
name='resubmissions',
field=models.ManyToManyField(blank=True, related_name='_ecoaccount_resubmissions_+', to='konova.Resubmission'),
),
]

View File

@ -0,0 +1,36 @@
# Generated by Django 3.1.3 on 2022-10-11 11:39
from django.db import migrations, models
from django.db.models import Sum
def fill_deductable_rest(apps, schema_editor):
EcoAccount = apps.get_model("compensation", "EcoAccount")
accs = EcoAccount.objects.all()
for acc in accs:
deductions = acc.deductions.filter(
intervention__deleted=None,
)
deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0
available_surfaces = acc.deductable_surface or deductions_surfaces
rest = available_surfaces - deductions_surfaces
acc.deductable_rest = rest
acc.save()
class Migration(migrations.Migration):
dependencies = [
('compensation', '0010_auto_20220815_1030'),
]
operations = [
migrations.AddField(
model_name='ecoaccount',
name='deductable_rest',
field=models.FloatField(blank=True, default=0, help_text='Amount of deductable rest', null=True),
),
migrations.RunPython(fill_deductable_rest)
]

View File

@ -0,0 +1,26 @@
# Generated by Django 3.1.3 on 2022-11-16 12:22
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('user', '0006_auto_20220815_0759'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('compensation', '0011_ecoaccount_deductable_rest'),
]
operations = [
migrations.AlterField(
model_name='ecoaccount',
name='teams',
field=models.ManyToManyField(blank=True, help_text='Teams having access (data shared with)', to='user.Team'),
),
migrations.AlterField(
model_name='ecoaccount',
name='users',
field=models.ManyToManyField(blank=True, help_text='Users having access (data shared with)', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -0,0 +1,35 @@
# Generated by Django 3.1.3 on 2022-11-17 07:19
from django.db import migrations
from compensation.models import UnitChoices
def harmonize_action_units(apps, schema_editor):
"""
CompensationAction units (based on UnitChoices) can be mixed up at this point where
* qm represents and
* m2 represents
We drop qm in support of m2
"""
CompensationAction = apps.get_model("compensation", "CompensationAction")
actions = CompensationAction.objects.filter(
unit="qm"
)
for action in actions:
action.unit = UnitChoices.m2
action.save()
class Migration(migrations.Migration):
dependencies = [
('compensation', '0012_auto_20221116_1322'),
]
operations = [
migrations.RunPython(harmonize_action_units),
]

View File

@ -0,0 +1,18 @@
# Generated by Django 3.1.3 on 2022-11-18 15:20
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('compensation', '0013_auto_20221117_0819'),
]
operations = [
migrations.AlterField(
model_name='compensationaction',
name='unit',
field=models.CharField(blank=True, choices=[('cm', 'cm'), ('m', 'm'), ('m2', ''), ('m3', ''), ('km', 'km'), ('ha', 'ha'), ('pcs', 'Pieces')], max_length=100, null=True),
),
]

View File

@ -0,0 +1,70 @@
# Generated by Django 4.2.6 on 2023-11-30 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('codelist', '0001_initial'),
('konova', '0014_resubmission'),
('compensation', '0014_auto_20221118_1620'),
]
operations = [
migrations.AlterField(
model_name='compensation',
name='after_states',
field=models.ManyToManyField(blank=True, help_text="Refers to 'Zielzustand Biotop'", related_name='+', to='compensation.compensationstate'),
),
migrations.AlterField(
model_name='compensation',
name='before_states',
field=models.ManyToManyField(blank=True, help_text="Refers to 'Ausgangszustand Biotop'", related_name='+', to='compensation.compensationstate'),
),
migrations.AlterField(
model_name='compensation',
name='deadlines',
field=models.ManyToManyField(blank=True, related_name='+', to='konova.deadline'),
),
migrations.AlterField(
model_name='compensation',
name='resubmissions',
field=models.ManyToManyField(blank=True, related_name='+', to='konova.resubmission'),
),
migrations.AlterField(
model_name='compensationaction',
name='action_type',
field=models.ManyToManyField(blank=True, limit_choices_to={'code_lists__in': [1026], 'is_archived': False, 'is_selectable': True}, related_name='+', to='codelist.konovacode'),
),
migrations.AlterField(
model_name='compensationaction',
name='action_type_details',
field=models.ManyToManyField(blank=True, limit_choices_to={'code_lists__in': [1035], 'is_archived': False, 'is_selectable': True}, related_name='+', to='codelist.konovacode'),
),
migrations.AlterField(
model_name='compensationstate',
name='biotope_type_details',
field=models.ManyToManyField(blank=True, limit_choices_to={'code_lists__in': [975], 'is_archived': False, 'is_selectable': True}, related_name='+', to='codelist.konovacode'),
),
migrations.AlterField(
model_name='ecoaccount',
name='after_states',
field=models.ManyToManyField(blank=True, help_text="Refers to 'Zielzustand Biotop'", related_name='+', to='compensation.compensationstate'),
),
migrations.AlterField(
model_name='ecoaccount',
name='before_states',
field=models.ManyToManyField(blank=True, help_text="Refers to 'Ausgangszustand Biotop'", related_name='+', to='compensation.compensationstate'),
),
migrations.AlterField(
model_name='ecoaccount',
name='deadlines',
field=models.ManyToManyField(blank=True, related_name='+', to='konova.deadline'),
),
migrations.AlterField(
model_name='ecoaccount',
name='resubmissions',
field=models.ManyToManyField(blank=True, related_name='+', to='konova.resubmission'),
),
]

View File

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

View File

@ -19,8 +19,9 @@ class UnitChoices(models.TextChoices):
""" """
cm = "cm", _("cm") cm = "cm", _("cm")
m = "m", _("m") m = "m", _("m")
m2 = "m2", _("")
m3 = "m3", _("")
km = "km", _("km") km = "km", _("km")
qm = "qm", _("")
ha = "ha", _("ha") ha = "ha", _("ha")
st = "pcs", _("Pieces") # pieces st = "pcs", _("Pieces") # pieces

View File

@ -8,27 +8,33 @@ Created on: 16.11.21
import shutil import shutil
from django.contrib import messages from django.contrib import messages
from django.urls import reverse
from analysis.settings import LKOMPVZVO_PUBLISH_DATE
from codelist.models import KonovaCode from codelist.models import KonovaCode
from compensation.settings import COMPENSATION_IDENTIFIER_TEMPLATE, COMPENSATION_IDENTIFIER_LENGTH, \
COMPENSATION_LANIS_LAYER_NAME_RECORDED, COMPENSATION_LANIS_LAYER_NAME_UNRECORDED, COMPENSATION_LANIS_LAYER_NAME_UNRECORDED_OLD_ENTRY
from konova.sub_settings.django_settings import BASE_URL
from user.models import User, Team from user.models import User, Team
from django.db import models, transaction from django.db import models, transaction
from django.db.models import QuerySet, Sum from django.db.models import QuerySet, Sum
from django.http import HttpRequest from django.http import HttpRequest
from django.utils.translation import gettext_lazy as _
from compensation.managers import CompensationManager from compensation.managers import CompensationManager
from compensation.models import CompensationState, CompensationAction from compensation.models import CompensationState, CompensationAction
from compensation.utils.quality import CompensationQualityChecker from compensation.utils.quality import CompensationQualityChecker
from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \ from konova.models import BaseObject, AbstractDocument, Deadline, generate_document_file_upload_path, \
GeoReferencedMixin GeoReferencedMixin, DeadlineType, ResubmitableObjectMixin
from konova.settings import DEFAULT_SRID_RLP, LANIS_LINK_TEMPLATE
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \ from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, COMPENSATION_REMOVED_TEMPLATE, \
DOCUMENT_REMOVED_TEMPLATE, COMPENSATION_EDITED_TEMPLATE, DEADLINE_REMOVED, ADDED_DEADLINE, \ DOCUMENT_REMOVED_TEMPLATE, DEADLINE_REMOVED, DEADLINE_ADDED, \
COMPENSATION_ACTION_REMOVED, COMPENSATION_STATE_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE COMPENSATION_ACTION_REMOVED, COMPENSATION_STATE_REMOVED, INTERVENTION_HAS_REVOCATIONS_TEMPLATE
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
class AbstractCompensation(BaseObject, GeoReferencedMixin): class AbstractCompensation(BaseObject,
GeoReferencedMixin,
ResubmitableObjectMixin
):
""" """
Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation, Abstract compensation model which holds basic attributes, shared by subclasses like the regular Compensation,
EMA or EcoAccount. EMA or EcoAccount.
@ -74,7 +80,7 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
self.save() self.save()
self.deadlines.add(deadline) self.deadlines.add(deadline)
self.mark_as_edited(user, edit_comment=ADDED_DEADLINE) self.mark_as_edited(user, edit_comment=DEADLINE_ADDED)
return deadline return deadline
def remove_deadline(self, form): def remove_deadline(self, form):
@ -198,7 +204,9 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
Returns: Returns:
""" """
return qs.aggregate(Sum("surface"))["surface__sum"] or 0 val = qs.aggregate(Sum("surface"))["surface__sum"] or 0
val = float('{:0.2f}'.format(val))
return val
def quality_check(self) -> CompensationQualityChecker: def quality_check(self) -> CompensationQualityChecker:
""" Performs data quality check """ Performs data quality check
@ -226,6 +234,15 @@ class AbstractCompensation(BaseObject, GeoReferencedMixin):
request = self.set_geometry_conflict_message(request) request = self.set_geometry_conflict_message(request)
return request return request
def get_finished_deadlines(self):
""" Getter for FINISHED-deadlines
Returns:
queryset (QuerySet): The finished deadlines
"""
return self.deadlines.filter(
type=DeadlineType.FINISHED
)
class CEFMixin(models.Model): class CEFMixin(models.Model):
""" Provides CEF flag as Mixin """ Provides CEF flag as Mixin
@ -286,9 +303,18 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
objects = CompensationManager() objects = CompensationManager()
identifier_length = COMPENSATION_IDENTIFIER_LENGTH
identifier_template = COMPENSATION_IDENTIFIER_TEMPLATE
def __str__(self): def __str__(self):
return "{}".format(self.identifier) return "{}".format(self.identifier)
def get_detail_url(self):
return reverse("compensation:detail", args=(self.id,))
def get_detail_url_absolute(self):
return BASE_URL + self.get_detail_url()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
# Create new identifier is none was given # Create new identifier is none was given
@ -318,6 +344,20 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
# Compensations inherit their shared state from the interventions # Compensations inherit their shared state from the interventions
return self.intervention.is_shared_with(user) return self.intervention.is_shared_with(user)
def is_only_shared_with(self, user: User):
""" Share check
Checks whether a given user is the only one having shared access to this entry
Args:
user (User): The user to be checked
Returns:
"""
# Compensations inherit their shared state from the interventions
return self.intervention.is_only_shared_with(user)
def share_with_user(self, user: User): def share_with_user(self, user: User):
""" Adds user to list of shared access users """ Adds user to list of shared access users
@ -369,7 +409,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
Returns: Returns:
users (QuerySet) users (QuerySet)
""" """
return self.intervention.users.all() return self.intervention.shared_users
@property @property
def shared_teams(self) -> QuerySet: def shared_teams(self) -> QuerySet:
@ -378,7 +418,7 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
Returns: Returns:
users (QuerySet) users (QuerySet)
""" """
return self.intervention.teams.all() return self.intervention.shared_teams
def get_documents(self) -> QuerySet: def get_documents(self) -> QuerySet:
""" Getter for all documents of a compensation """ Getter for all documents of a compensation
@ -391,19 +431,18 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
) )
return docs return docs
def mark_as_edited(self, user: User, request: HttpRequest = None, edit_comment: str = None, reset_recorded: bool = True): def mark_as_edited(self, user: User, request: HttpRequest = None, edit_comment: str = None):
""" Performs internal logic for setting the recordedd/checked state of the related intervention """ Performs internal logic for setting the checked state of the related intervention
Args: Args:
user (User): The performing user user (User): The performing user
request (HttpRequest): The performing request request (HttpRequest): The performing request
edit_comment (str): Additional comment for the log entry edit_comment (str): Additional comment for the log entry
reset_recorded (bool): Whether the record-state of the object should be reset
Returns: Returns:
""" """
self.intervention.unrecord(user, request) self.intervention.set_unchecked()
action = super().mark_as_edited(user, edit_comment=edit_comment) action = super().mark_as_edited(user, edit_comment=edit_comment)
return action return action
@ -449,6 +488,28 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
""" """
return self.intervention.is_recorded return self.intervention.is_recorded
def get_lanis_layer_name(self):
""" Getter for specific LANIS/WFS object layer
Returns:
"""
retval = None
if self.is_recorded:
retval = COMPENSATION_LANIS_LAYER_NAME_RECORDED
else:
try:
is_old_entry = self.intervention.legal.binding_date < LKOMPVZVO_PUBLISH_DATE
except TypeError:
is_old_entry = False
if is_old_entry:
retval = COMPENSATION_LANIS_LAYER_NAME_UNRECORDED_OLD_ENTRY
else:
retval = COMPENSATION_LANIS_LAYER_NAME_UNRECORDED
return retval
class CompensationDocument(AbstractDocument): class CompensationDocument(AbstractDocument):
""" """
@ -483,8 +544,11 @@ class CompensationDocument(AbstractDocument):
# The only file left for this compensation is the one which is currently processed and will be deleted # The only file left for this compensation is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file # Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path # Therefore take the folder path from the file path
folder_path = self.file.path.split("/")[:-1] try:
folder_path = "/".join(folder_path) folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
except ValueError:
folder_path = None
if user: if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title)) self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))

View File

@ -9,18 +9,20 @@ import shutil
from django.urls import reverse from django.urls import reverse
from compensation.settings import ECO_ACCOUNT_IDENTIFIER_TEMPLATE, ECO_ACCOUNT_IDENTIFIER_LENGTH, \
ECO_ACCOUNT_LANIS_LAYER_NAME_RECORDED, ECO_ACCOUNT_LANIS_LAYER_NAME_UNRECORDED
from konova.sub_settings.django_settings import BASE_URL
from konova.utils.message_templates import DEDUCTION_REMOVED, DOCUMENT_REMOVED_TEMPLATE from konova.utils.message_templates import DEDUCTION_REMOVED, DOCUMENT_REMOVED_TEMPLATE
from django.core.exceptions import ValidationError
from django.core.validators import MinValueValidator from django.core.validators import MinValueValidator
from django.db import models from django.db import models
from django.db.models import Sum, QuerySet from django.db.models import Sum, QuerySet
from django.utils.translation import gettext_lazy as _
from compensation.managers import EcoAccountManager, EcoAccountDeductionManager from compensation.managers import EcoAccountManager, EcoAccountDeductionManager
from compensation.models.compensation import AbstractCompensation, PikMixin from compensation.models.compensation import AbstractCompensation, PikMixin
from compensation.utils.quality import EcoAccountQualityChecker from compensation.utils.quality import EcoAccountQualityChecker
from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \ from konova.models import ShareableObjectMixin, RecordableObjectMixin, AbstractDocument, BaseResource, \
generate_document_file_upload_path generate_document_file_upload_path
from konova.tasks import celery_send_mail_deduction_changed, celery_send_mail_deduction_changed_team
class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin): class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, PikMixin):
@ -34,6 +36,12 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
help_text="Amount of deductable surface - can be lower than the total surface due to deduction limitations", help_text="Amount of deductable surface - can be lower than the total surface due to deduction limitations",
default=0, default=0,
) )
deductable_rest = models.FloatField(
blank=True,
null=True,
help_text="Amount of deductable rest",
default=0,
)
legal = models.OneToOneField( legal = models.OneToOneField(
"intervention.Legal", "intervention.Legal",
@ -45,22 +53,17 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
objects = EcoAccountManager() objects = EcoAccountManager()
identifier_length = ECO_ACCOUNT_IDENTIFIER_LENGTH
identifier_template = ECO_ACCOUNT_IDENTIFIER_TEMPLATE
def __str__(self): def __str__(self):
return f"{self.identifier} ({self.title})" return f"{self.identifier} ({self.title})"
def clean(self): def get_detail_url(self):
# Deductable surface can not be larger than added states after surface return reverse("compensation:acc:detail", args=(self.id,))
after_state_sum = self.get_state_after_surface_sum()
if self.deductable_surface > after_state_sum:
raise ValidationError(_("Deductable surface can not be larger than existing surfaces in after states"))
# Deductable surface can not be lower than amount of already deducted surfaces def get_detail_url_absolute(self):
# User needs to contact deducting user in case of further problems return BASE_URL + self.get_detail_url()
deducted_sum = self.get_deductions_surface()
if self.deductable_surface < deducted_sum:
raise ValidationError(
_("Deductable surface can not be smaller than the sum of already existing deductions. Please contact the responsible users for the deductions!")
)
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
@ -89,38 +92,30 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
Returns: Returns:
sum_surface (float) sum_surface (float)
""" """
return self.deductions.all().aggregate(Sum("surface"))["surface__sum"] or 0 val = self.deductions.all().aggregate(Sum("surface"))["surface__sum"] or 0
val = float('{:0.2f}'.format(val))
return val
def get_state_after_surface_sum(self) -> float: def __calculate_deductable_rest(self):
""" Calculates the account's after state surface sum
Returns:
sum_surface (float)
"""
return self.after_states.all().aggregate(Sum("surface"))["surface__sum"] or 0
def get_available_rest(self) -> (float, float):
""" Calculates available rest surface of the eco account """ Calculates available rest surface of the eco account
Args: Args:
Returns: Returns:
ret_val_total (float): Total amount ret_val_total (float): Total amount
ret_val_relative (float): Amount as percentage (0-100)
""" """
deductions = self.deductions.filter( deductions_surfaces = self.get_deductions_surface()
intervention__deleted=None,
)
deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0
available_surfaces = self.deductable_surface or deductions_surfaces ## no division by zero
ret_val_total = available_surfaces - deductions_surfaces
if available_surfaces > 0: available_surface = self.deductable_surface
ret_val_relative = int((ret_val_total / available_surfaces) * 100) if available_surface is None:
# Fallback!
available_surface = deductions_surfaces
else: else:
ret_val_relative = 0 available_surface = float(available_surface)
return ret_val_total, ret_val_relative ret_val = available_surface - deductions_surfaces
return ret_val
def quality_check(self) -> EcoAccountQualityChecker: def quality_check(self) -> EcoAccountQualityChecker:
""" Quality check """ Quality check
@ -159,8 +154,63 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
Returns: Returns:
""" """
return reverse("compensation:acc:share", args=(self.id, self.access_token)) return reverse("compensation:acc:share-token", args=(self.id, self.access_token))
def send_notification_mail_on_deduction_change(self, data_change: dict):
""" Sends notification mails for changes on the deduction
Args:
data_change ():
Returns:
"""
# Send mail
shared_users = self.shared_users.values_list("id", flat=True)
for user_id in shared_users:
celery_send_mail_deduction_changed.delay(self.id, self.get_app_object_tuple(), user_id, data_change)
# Send mail
shared_teams = self.shared_teams.values_list("id", flat=True)
for team_id in shared_teams:
celery_send_mail_deduction_changed_team.delay(self.id, self.get_app_object_tuple(), team_id, data_change)
def update_deductable_rest(self):
"""
Updates deductable_rest, which holds the amount of rest surface for this account.
Returns:
"""
self.deductable_rest = self.__calculate_deductable_rest()
self.save()
def get_deductable_rest_relative(self):
"""
Returns deductable_rest relative to deductable_surface mapped to [0,100]
Returns:
"""
try:
ret_val = int((self.deductable_rest / (self.deductable_surface or 0)) * 100)
except ZeroDivisionError:
ret_val = 0
return ret_val
def get_lanis_layer_name(self):
""" Getter for specific LANIS/WFS object layer
Returns:
"""
retval = None
if self.is_recorded:
retval = ECO_ACCOUNT_LANIS_LAYER_NAME_RECORDED
else:
retval = ECO_ACCOUNT_LANIS_LAYER_NAME_UNRECORDED
return retval
class EcoAccountDocument(AbstractDocument): class EcoAccountDocument(AbstractDocument):
""" """
@ -195,8 +245,11 @@ class EcoAccountDocument(AbstractDocument):
# The only file left for this eco account is the one which is currently processed and will be deleted # The only file left for this eco account is the one which is currently processed and will be deleted
# Make sure that the compensation folder itself is deleted as well, not only the file # Make sure that the compensation folder itself is deleted as well, not only the file
# Therefore take the folder path from the file path # Therefore take the folder path from the file path
folder_path = self.file.path.split("/")[:-1] try:
folder_path = "/".join(folder_path) folder_path = self.file.path.split("/")[:-1]
folder_path = "/".join(folder_path)
except ValueError:
folder_path = None
if user: if user:
self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title)) self.instance.mark_as_edited(user, edit_comment=DOCUMENT_REMOVED_TEMPLATE.format(self.title))
@ -252,3 +305,8 @@ class EcoAccountDeduction(BaseResource):
self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED) self.intervention.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED) self.account.mark_as_edited(user, edit_comment=DEDUCTION_REMOVED)
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
self.account.update_deductable_rest()
def save(self, *args, **kwargs):
super().save(*args, **kwargs)
self.account.update_deductable_rest()

View File

@ -10,8 +10,6 @@ from django.db import models
from intervention.models import Intervention from intervention.models import Intervention
from konova.models import BaseResource from konova.models import BaseResource
from konova.utils.message_templates import PAYMENT_REMOVED
from user.models import UserActionLogEntry
class Payment(BaseResource): class Payment(BaseResource):

View File

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

View File

@ -7,6 +7,11 @@ Created on: 18.12.20
""" """
COMPENSATION_IDENTIFIER_LENGTH = 6 COMPENSATION_IDENTIFIER_LENGTH = 6
COMPENSATION_IDENTIFIER_TEMPLATE = "KOM-{}" COMPENSATION_IDENTIFIER_TEMPLATE = "KOM-{}"
COMPENSATION_LANIS_LAYER_NAME_RECORDED = "kom_recorded"
COMPENSATION_LANIS_LAYER_NAME_UNRECORDED = "kom_unrecorded"
COMPENSATION_LANIS_LAYER_NAME_UNRECORDED_OLD_ENTRY = "kom_unrecorded_old_entries"
ECO_ACCOUNT_IDENTIFIER_LENGTH = 6 ECO_ACCOUNT_IDENTIFIER_LENGTH = 6
ECO_ACCOUNT_IDENTIFIER_TEMPLATE = "OEK-{}" ECO_ACCOUNT_IDENTIFIER_TEMPLATE = "OEK-{}"
ECO_ACCOUNT_LANIS_LAYER_NAME_RECORDED = "oek_recorded"
ECO_ACCOUNT_LANIS_LAYER_NAME_UNRECORDED = "oek_unrecorded"

View File

@ -1,346 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 01.12.20
"""
from konova.utils.message_templates import DATA_IS_UNCHECKED, DATA_CHECKED_ON_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE
from django.http import HttpRequest
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from compensation.filters import CompensationTableFilter, EcoAccountTableFilter
from compensation.models import Compensation, EcoAccount
from konova.utils.tables import BaseTable, TableRenderMixin
import django_tables2 as tables
class CompensationTable(BaseTable, TableRenderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
accessor="geometry",
)
c = tables.Column(
verbose_name=_("Checked"),
orderable=True,
empty_values=[],
accessor="intervention__checked",
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
empty_values=[],
accessor="intervention__recorded",
)
e = tables.Column(
verbose_name=_("Editable"),
orderable=True,
empty_values=[],
accessor="intervention__users",
)
lm = tables.Column(
verbose_name=_("Last edit"),
orderable=True,
accessor="modified__timestamp",
)
class Meta(BaseTable.Meta):
template_name = "django_tables2/bootstrap4.html"
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Compensations")
self.add_new_url = reverse("compensation:new")
qs = kwargs.get("queryset", None)
self.filter = CompensationTableFilter(
user=request.user,
data=request.GET,
queryset=qs,
)
kwargs["queryset"] = self.filter.qs
super().__init__(request, *args, **kwargs)
def render_id(self, value, record: Compensation):
""" Renders the id column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
context = {
"tooltip": _("Open {}").format(_("Intervention")),
"content": value,
"url": reverse("compensation:detail", args=(record.id,)),
"has_revocations": record.intervention.legal.revocations.exists()
}
html = render_to_string(
"table/revocation_warning_col.html",
context
)
return html
def render_c(self, value, record: Compensation):
""" Renders the checked column for a compensation
checked is set by the main object Intervention
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
html = ""
checked = value is not None
tooltip = DATA_IS_UNCHECKED
previously_checked = record.intervention.get_last_checked_action()
if checked:
checked_on = value.get_timestamp_str_formatted()
tooltip = DATA_CHECKED_ON_TEMPLATE.format(checked_on, record.intervention.checked.user)
html += self.render_checked_star(
tooltip=tooltip,
icn_filled=checked,
)
if previously_checked and not checked:
checked_on = previously_checked.get_timestamp_str_formatted()
tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(checked_on, previously_checked.user)
html += self.render_previously_checked_star(
tooltip=tooltip,
)
return format_html(html)
def render_d(self, value, record: Compensation):
""" Renders the parcel district column for a compensation
Args:
value (str): The geometry
record (Compensation): The compensation record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels
}
)
return html
def render_r(self, value, record: Compensation):
""" Renders the registered column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
html = ""
recorded = value is not None
tooltip = _("Not recorded yet")
if recorded:
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
icn_filled=recorded,
)
return format_html(html)
def render_e(self, value, record: Compensation):
""" Renders the editable column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
has_access = record.is_shared_with(self.user)
html = self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
)
return format_html(html)
class EcoAccountTable(BaseTable, TableRenderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=True,
accessor="geometry",
)
av = tables.Column(
verbose_name=_("Available"),
orderable=True,
empty_values=[],
attrs={
"th": {
"class": "w-20",
}
}
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
empty_values=[],
accessor="recorded",
)
e = tables.Column(
verbose_name=_("Editable"),
orderable=True,
empty_values=[],
accessor="users",
)
lm = tables.Column(
verbose_name=_("Last edit"),
orderable=True,
accessor="modified__timestamp",
)
class Meta(BaseTable.Meta):
template_name = "django_tables2/bootstrap4.html"
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Eco Accounts")
self.add_new_url = reverse("compensation:acc:new")
qs = kwargs.get("queryset", None)
self.filter = EcoAccountTableFilter(
user=request.user,
data=request.GET,
queryset=qs,
)
kwargs["queryset"] = self.filter.qs
super().__init__(request, *args, **kwargs)
def render_id(self, value, record: EcoAccount):
""" Renders the id column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
html += self.render_link(
tooltip=_("Open {}").format(_("Eco-account")),
href=reverse("compensation:acc:detail", args=(record.id,)),
txt=value,
new_tab=False,
)
return format_html(html)
def render_av(self, value, record: EcoAccount):
""" Renders the available column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
value_total, value_relative = record.get_available_rest()
html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative})
return format_html(html)
def render_d(self, value, record: Compensation):
""" Renders the parcel district column for a compensation
Args:
value (str): The geometry
record (Compensation): The compensation record
Returns:
"""
parcels = value.get_underlying_parcels().values_list(
"parcel_group__name",
flat=True
).distinct()
html = render_to_string(
"table/gmrkng_col.html",
{
"entries": parcels
}
)
return html
def render_r(self, value, record: EcoAccount):
""" Renders the recorded column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
checked = value is not None
tooltip = _("Not recorded yet. Can not be used for deductions, yet.")
if checked:
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
icn_filled=checked,
)
return format_html(html)
def render_e(self, value, record: EcoAccount):
""" Renders the editable column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
# Do not use value in here, since value does use unprefetched 'users' manager, where record has already
# prefetched users data
has_access = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
)
return format_html(html)

View File

@ -0,0 +1,7 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""

View File

@ -0,0 +1,150 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django.http import HttpRequest
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from compensation.filters.compensation import CompensationTableFilter
from compensation.models import Compensation
from konova.utils.message_templates import DATA_IS_UNCHECKED, DATA_CHECKED_ON_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE
from konova.utils.tables import BaseTable, TableRenderMixin, TableOrderMixin
import django_tables2 as tables
class CompensationTable(BaseTable, TableRenderMixin, TableOrderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=False,
accessor="geometry",
)
c = tables.Column(
verbose_name=_("Checked"),
orderable=True,
empty_values=[],
accessor="intervention__checked",
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
empty_values=[],
accessor="intervention__recorded",
)
e = tables.Column(
verbose_name=_("Editable"),
orderable=True,
empty_values=[],
accessor="intervention__users",
)
lm = tables.Column(
verbose_name=_("Last edit"),
orderable=True,
accessor="modified__timestamp",
)
class Meta(BaseTable.Meta):
template_name = "django_tables2/bootstrap4.html"
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Compensations")
self.add_new_url = reverse("compensation:new")
qs = kwargs.get("queryset", None)
self.filter = CompensationTableFilter(
user=request.user,
data=request.GET,
queryset=qs,
)
kwargs["queryset"] = self.filter.qs
super().__init__(request, *args, **kwargs)
def render_id(self, value, record: Compensation):
""" Renders the id column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
context = {
"tooltip": _("Open {}").format(_("Intervention")),
"content": value,
"url": reverse("compensation:detail", args=(record.id,)),
"has_revocations": record.intervention.legal.revocations.exists()
}
html = render_to_string(
"table/revocation_warning_col.html",
context
)
return html
def render_c(self, value, record: Compensation):
""" Renders the checked column for a compensation
checked is set by the main object Intervention
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
html = ""
checked = value is not None
tooltip = DATA_IS_UNCHECKED
previously_checked = record.intervention.get_last_checked_action()
if checked:
checked_on = value.get_timestamp_str_formatted()
tooltip = DATA_CHECKED_ON_TEMPLATE.format(checked_on, record.intervention.checked.user)
html += self.render_checked_star(
tooltip=tooltip,
icn_filled=checked,
)
if previously_checked and not checked:
checked_on = previously_checked.get_timestamp_str_formatted()
tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(checked_on, previously_checked.user)
html += self.render_previously_checked_star(
tooltip=tooltip,
)
return format_html(html)
def render_r(self, value, record: Compensation):
""" Renders the registered column for a compensation
Args:
value (str): The identifier value
record (Compensation): The compensation record
Returns:
"""
html = ""
recorded = value is not None
tooltip = _("Not recorded yet")
if recorded:
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.intervention.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
icn_filled=recorded,
)
return format_html(html)

View File

@ -0,0 +1,138 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22
"""
from django.http import HttpRequest
from django.template.loader import render_to_string
from django.urls import reverse
from django.utils.formats import number_format
from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _
from compensation.filters.eco_account import EcoAccountTableFilter
from compensation.models import EcoAccount
from konova.utils.tables import TableRenderMixin, BaseTable, TableOrderMixin
import django_tables2 as tables
class EcoAccountTable(BaseTable, TableRenderMixin, TableOrderMixin):
id = tables.Column(
verbose_name=_("Identifier"),
orderable=True,
accessor="identifier",
)
t = tables.Column(
verbose_name=_("Title"),
orderable=True,
accessor="title",
)
d = tables.Column(
verbose_name=_("Parcel gmrkng"),
orderable=False,
accessor="geometry",
)
av = tables.Column(
verbose_name=_("Available"),
orderable=True,
accessor="deductable_rest",
attrs={
"th": {
"class": "w-20",
}
}
)
r = tables.Column(
verbose_name=_("Recorded"),
orderable=True,
empty_values=[],
accessor="recorded",
)
e = tables.Column(
verbose_name=_("Editable"),
orderable=True,
empty_values=[],
accessor="users",
)
lm = tables.Column(
verbose_name=_("Last edit"),
orderable=True,
accessor="modified__timestamp",
)
class Meta(BaseTable.Meta):
template_name = "django_tables2/bootstrap4.html"
def __init__(self, request: HttpRequest, *args, **kwargs):
self.title = _("Eco Accounts")
self.add_new_url = reverse("compensation:acc:new")
qs = kwargs.get("queryset", None)
self.filter = EcoAccountTableFilter(
user=request.user,
data=request.GET,
queryset=qs,
)
kwargs["queryset"] = self.filter.qs
super().__init__(request, *args, **kwargs)
def render_id(self, value, record: EcoAccount):
""" Renders the id column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
html += self.render_link(
tooltip=_("Open {}").format(_("Eco-account")),
href=reverse("compensation:acc:detail", args=(record.id,)),
txt=value,
new_tab=False,
)
return format_html(html)
def render_av(self, value, record: EcoAccount):
""" Renders the available column for an eco account
Args:
value (float): The deductable_rest
record (EcoAccount): The eco account record
Returns:
"""
try:
value_relative = record.get_deductable_rest_relative()
except ZeroDivisionError:
value_relative = 0
html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative})
html += f"{number_format(record.deductable_rest, decimal_pos=2)}"
return format_html(html)
def render_r(self, value, record: EcoAccount):
""" Renders the recorded column for an eco account
Args:
value (str): The identifier value
record (EcoAccount): The eco account record
Returns:
"""
html = ""
checked = value is not None
tooltip = _("Not recorded yet. Can not be used for deductions, yet.")
if checked:
on = value.get_timestamp_str_formatted()
tooltip = _("Recorded on {} by {}").format(on, record.recorded.user)
html += self.render_bookmark(
tooltip=tooltip,
icn_filled=checked,
)
return format_html(html)

View File

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

View File

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

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and has_access %} {% if is_default_member and is_entry_shared %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-deadline' obj.id %}" title="{% trans 'Add new deadline' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'calendar-check' %} {% fa5_icon 'calendar-check' %}
@ -20,7 +20,12 @@
</div> </div>
</div> </div>
</div> </div>
<div class="card-body scroll-300 p-2"> {% if not has_finished_deadlines %}
<div class="alert alert-danger mb-0">
{% trans 'Missing finished deadline ' %}
</div>
{% endif %}
<div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -33,7 +38,7 @@
<th scope="col"> <th scope="col">
{% trans 'Comment' %} {% trans 'Comment' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and is_entry_shared %}
<th class="w-10" scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
@ -55,7 +60,7 @@
</div> </div>
</td> </td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and is_entry_shared %}
<button data-form-url="{% url 'compensation:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}"> <button data-form-url="{% url 'compensation:deadline-edit' obj.id deadline.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit deadline' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

View File

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

View File

@ -10,7 +10,7 @@
</div> </div>
<div class="col-sm-6"> <div class="col-sm-6">
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
{% if is_default_member and has_access %} {% if is_default_member and is_entry_shared %}
<button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}" title="{% trans 'Add new state after' %}"> <button class="btn btn-outline-default btn-modal" data-form-url="{% url 'compensation:new-state' obj.id %}" title="{% trans 'Add new state after' %}">
{% fa5_icon 'plus' %} {% fa5_icon 'plus' %}
{% fa5_icon 'layer-group' %} {% fa5_icon 'layer-group' %}
@ -25,7 +25,7 @@
{% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m² {% trans 'Missing surfaces according to states before: ' %}{{ diff_states|floatformat:2 }} m²
</div> </div>
{% endif %} {% endif %}
<div class="card-body scroll-300 p-2"> <div class="card-body {% if tables_scrollable %}scroll-300{% endif %} p-2">
<table class="table table-hover"> <table class="table table-hover">
<thead> <thead>
<tr> <tr>
@ -35,7 +35,7 @@
<th scope="col"> <th scope="col">
{% trans 'Surface' %} {% trans 'Surface' %}
</th> </th>
{% if is_default_member and has_access %} {% if is_default_member and is_entry_shared %}
<th class="w-10" scope="col"> <th class="w-10" scope="col">
<span class="float-right"> <span class="float-right">
{% trans 'Action' %} {% trans 'Action' %}
@ -51,14 +51,14 @@
<span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span> <span>{{ state.biotope_type.parent.long_name }} {% fa5_icon 'angle-right' %} {{ state.biotope_type.long_name }} ({{state.biotope_type.short_name}})</span>
<br> <br>
{% for detail in state.biotope_type_details.all %} {% for detail in state.biotope_type_details.all %}
<span class="badge badge-pill rlp-r" title="{{detail}}">{{detail.long_name}}</span> <span class="badge badge-pill rlp-r" title="{{ detail.parent.short_name }} > {{detail}}">{{ detail.parent.short_name }} > {{ detail.long_name }}</span>
{% empty %} {% empty %}
<span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span> <span class="badge badge-pill rlp-r-outline" title="{% trans 'No biotope type details' %}">{% trans 'No biotope type details' %}</span>
{% endfor %} {% endfor %}
</td> </td>
<td>{{ state.surface|floatformat:2 }} m²</td> <td>{{ state.surface|floatformat:2 }} m²</td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and has_access %} {% if is_default_member and is_entry_shared %}
<button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}"> <button data-form-url="{% url 'compensation:state-edit' obj.id state.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit state' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>
@ -68,6 +68,10 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
{% empty %}
<div class="alert alert-danger mb-0">
{% trans 'Missing' %}
</div>
{% endfor %} {% endfor %}
</tbody> </tbody>
</table> </table>

View File

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

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