Compare commits

...

217 Commits
v1.1 ... 1.9

Author SHA1 Message Date
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
258 changed files with 7557 additions and 9482 deletions

46
.env.sample Normal file
View File

@@ -0,0 +1,46 @@
# 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
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
# 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

5
.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,7 @@ 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..."), help_text=_("Entries created from..."),
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
@@ -35,6 +37,7 @@ 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..."), help_text=_("Entries created until..."),
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={

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

@@ -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,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

@@ -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

@@ -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

@@ -413,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,
@@ -516,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,7 +45,6 @@ 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")
@@ -48,3 +53,105 @@ class APIUserToken(models.Model):
except ObjectDoesNotExist: except ObjectDoesNotExist:
raise PermissionError("Credentials invalid") raise PermissionError("Credentials invalid")
return token_obj.user return token_obj.user
class OAuthToken(UuidModel):
access_token = models.CharField(
max_length=255,
blank=False,
null=False,
db_comment="OAuth access token"
)
refresh_token = models.CharField(
max_length=255,
blank=False,
null=False,
db_comment="OAuth refresh token"
)
expires_on = models.DateTimeField(
db_comment="When the token will be expired"
)
ASSUMED_LATENCY = 1000 # assumed latency between creation and receiving of an access token
def __str__(self):
return str(self.access_token)
@staticmethod
def from_access_token_response(access_token_data: str, received_on):
"""
Creates an OAuthToken based on retrieved access token data (OAuth2.0 specification)
Args:
access_token_data (str): OAuth2.0 response data
received_on (): Timestamp when the response has been received
Returns:
"""
oauth_token = OAuthToken()
data = json.loads(access_token_data)
oauth_token.access_token = data.get("access_token")
oauth_token.refresh_token = data.get("refresh_token")
expires_on = received_on + timedelta(
seconds=(data.get("expires_in") + OAuthToken.ASSUMED_LATENCY)
)
oauth_token.expires_on = expires_on
return oauth_token
def refresh(self):
url = f"{SSO_SERVER_BASE}o/token/"
params = {
"grant_type": "refresh_token",
"refresh_token": self.refresh_token,
"client_id": OAUTH_CLIENT_ID,
"client_secret": OAUTH_CLIENT_SECRET
}
response = requests.post(
url,
params
)
_now = now()
is_response_invalid = response.status_code != 200
if is_response_invalid:
raise RuntimeError(f"Refreshing token not possible: {response.status_code}")
response_content = response.content.decode("utf-8")
response_content = json.loads(response_content)
access_token = response_content.get("access_token")
refresh_token = response_content.get("refresh_token")
expires_in = response_content.get("expires")
self.access_token = access_token
self.refresh_token = refresh_token
self.expires_in = expires_in
self.save()
return self
def update_and_get_user(self):
from user.models import User
url = f"{SSO_SERVER_BASE}users/oauth/data/"
access_token = self.access_token
response = requests.get(
url,
headers={
"Authorization": f"Bearer {access_token}",
}
)
is_response_code_invalid = response.status_code != 200
if is_response_code_invalid:
raise RuntimeError(f"OAuth user data fetching unsuccessful: {response.status_code}")
response_content = response.content.decode("utf-8")
response_content = json.loads(response_content)
user = User.oauth_update_user(response_content)
return user

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

@@ -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

@@ -11,6 +11,7 @@ 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.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
@@ -32,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
@@ -76,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
@@ -86,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)

View File

@@ -6,6 +6,7 @@ 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
@@ -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 {

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

@@ -133,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):
@@ -201,6 +200,7 @@ class InterventionAPISerializerV1(AbstractModelAPISerializerV1,
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)

View File

@@ -61,7 +61,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,
} }
@@ -70,7 +70,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:
@@ -83,7 +83,7 @@ class AbstractModelAPISerializerV1(AbstractModelAPISerializer):
return None return None
try: try:
code = KonovaCode.objects.get( code = KonovaCode.objects.get(
atom_id=json_str, id=json_str,
code_lists__in=[code_list_identifier] code_lists__in=[code_list_identifier]
) )
except ObjectDoesNotExist as e: except ObjectDoesNotExist as e:
@@ -297,9 +297,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:
@@ -341,11 +344,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_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:
@@ -354,7 +360,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
@@ -385,16 +391,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"]) ]
# Mapping of old "qm" into "m²" amount = float(entry["amount"])
unit = UnitChoices.m2.value if entry["unit"] == "qm" else entry["unit"] # Mapping of old "qm" into "m²"
comment = entry["comment"] 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
@@ -53,7 +52,13 @@ class AbstractAPIView(View):
# 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) ksp_token = request.headers.get(KSP_TOKEN_HEADER_IDENTIFIER, None)
ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None) ksp_user = request.headers.get(KSP_USER_HEADER_IDENTIFIER, None)
self.user = APIUserToken.get_user_from_token(ksp_token, ksp_user) token_user = APIUserToken.get_user_from_token(ksp_token)
if ksp_user != token_user.username:
raise PermissionError(f"Invalid token for {ksp_user}")
else:
self.user = token_user
request.user = self.user request.user = self.user
if not self.user.is_default_user(): if not self.user.is_default_user():
raise PermissionError("Default permissions required") raise PermissionError("Default permissions required")
@@ -315,7 +320,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

@@ -75,7 +75,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

@@ -148,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

@@ -32,3 +32,9 @@ class EcoAccountAutocomplete(Select2QuerySetView):
Q(title__icontains=self.q) Q(title__icontains=self.q)
).distinct() ).distinct()
return qs return qs
def get_result_label(self, result):
return str(result)
def get_selected_result_label(self, result):
return str(result)

View File

@@ -55,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

View File

@@ -30,11 +30,12 @@ class AbstractCompensationForm(BaseForm):
label=_("Identifier"), label=_("Identifier"),
label_suffix="", label_suffix="",
max_length=255, max_length=255,
help_text=_("Generated automatically"), help_text=_("Generated automatically - not editable"),
widget=GenerateInput( widget=GenerateInput(
attrs={ attrs={
"class": "form-control", "class": "form-control",
"url": None, # Needs to be set in inheriting constructors "url": None, # Needs to be set in inheriting constructors
"readonly": True,
} }
) )
) )
@@ -212,7 +213,6 @@ class EditCompensationForm(NewCompensationForm):
action = UserActionLogEntry.get_edited_action(user) action = UserActionLogEntry.get_edited_action(user)
# Fetch data from cleaned POST values # Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None) intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None) is_cef = self.cleaned_data.get("is_cef", None)
@@ -220,7 +220,6 @@ class EditCompensationForm(NewCompensationForm):
is_pik = self.cleaned_data.get("is_pik", None) is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None) comment = self.cleaned_data.get("comment", None)
self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.intervention = intervention self.instance.intervention = intervention
self.instance.is_cef = is_cef self.instance.is_cef = is_cef

View File

@@ -15,6 +15,7 @@ from compensation.models import EcoAccount
from intervention.models import Handler, Responsibility, Legal from intervention.models import Handler, Responsibility, Legal
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm from konova.forms.modals import RemoveModalForm
from konova.utils import validators
from user.models import User, UserActionLogEntry from user.models import User, UserActionLogEntry
@@ -43,6 +44,7 @@ class NewEcoAccountForm(AbstractCompensationForm, CompensationResponsibleFormMix
label_suffix="", label_suffix="",
help_text=_("When did the parties agree on this?"), help_text=_("When did the parties agree on this?"),
required=False, required=False,
validators=[validators.reasonable_date],
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
"type": "date", "type": "date",
@@ -170,10 +172,26 @@ class EditEcoAccountForm(NewEcoAccountForm):
disabled_fields 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): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
# Fetch data from cleaned POST values # Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None) registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None) handler_type = self.cleaned_data.get("handler_type", None)
@@ -200,7 +218,6 @@ class EditEcoAccountForm(NewEcoAccountForm):
self.instance.legal.save() self.instance.legal.save()
# Update main oject data # Update main oject data
self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.deductable_surface = surface self.instance.deductable_surface = surface
self.instance.comment = comment self.instance.comment = comment

View File

@@ -93,7 +93,7 @@ class NewCompensationActionModalForm(BaseModalForm):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("New action") self.form_title = _("New action")
self.form_caption = _("Insert data for the new action") self.form_caption = _("Insert data for the new action")
choices =KonovaCode.objects.filter( choices = KonovaCode.objects.filter(
code_lists__in=[CODELIST_COMPENSATION_ACTION_ID], code_lists__in=[CODELIST_COMPENSATION_ACTION_ID],
is_archived=False, is_archived=False,
is_leaf=True, is_leaf=True,

View File

@@ -10,6 +10,7 @@ from django.utils.translation import gettext_lazy as _
from konova.forms.modals import BaseModalForm from konova.forms.modals import BaseModalForm
from konova.models import DeadlineType from konova.models import DeadlineType
from konova.utils import validators
from konova.utils.message_templates import DEADLINE_EDITED from konova.utils.message_templates import DEADLINE_EDITED
@@ -34,6 +35,7 @@ class NewDeadlineModalForm(BaseModalForm):
label_suffix="", label_suffix="",
required=True, required=True,
help_text=_("Select date"), help_text=_("Select date"),
validators=[validators.reasonable_date],
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
"type": "date", "type": "date",
@@ -63,6 +65,22 @@ class NewDeadlineModalForm(BaseModalForm):
self.form_title = _("New deadline") self.form_title = _("New deadline")
self.form_caption = _("Insert data for the 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): def save(self):
deadline = self.instance.add_deadline(self) deadline = self.instance.add_deadline(self)
return deadline return deadline

View File

@@ -9,6 +9,7 @@ from django import forms
from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _ from django.utils.translation import pgettext_lazy as _con, gettext_lazy as _
from konova.forms.modals import RemoveModalForm, BaseModalForm from konova.forms.modals import RemoveModalForm, BaseModalForm
from konova.utils import validators
from konova.utils.message_templates import PAYMENT_EDITED from konova.utils.message_templates import PAYMENT_EDITED
@@ -33,6 +34,7 @@ class NewPaymentForm(BaseModalForm):
label=_("Due on"), label=_("Due on"),
label_suffix=_(""), label_suffix=_(""),
required=False, required=False,
validators=[validators.reasonable_date],
help_text=_("Due on which date"), help_text=_("Due on which date"),
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
@@ -75,8 +77,11 @@ class NewPaymentForm(BaseModalForm):
is_valid (bool): True if valid, False otherwise is_valid (bool): True if valid, False otherwise
""" """
super_valid = super().is_valid() super_valid = super().is_valid()
date = self.cleaned_data["due"] if not super_valid:
comment = self.cleaned_data["comment"] or None return super_valid
date = self.cleaned_data.get("due", None)
comment = self.cleaned_data.get("comment", None)
if not date and not comment: if not date and not comment:
# At least one needs to be set! # At least one needs to be set!
self.add_error( self.add_error(

View File

@@ -5,7 +5,7 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.08.22 Created on: 18.08.22
""" """
from bootstrap_modal_forms.utils import is_ajax from bootstrap_modal_forms.mixins import is_ajax
from dal import autocomplete from dal import autocomplete
from django import forms from django import forms
from django.contrib import messages from django.contrib import messages

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

@@ -8,8 +8,13 @@ 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
@@ -21,7 +26,7 @@ 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, DeadlineType, ResubmitableObjectMixin GeoReferencedMixin, DeadlineType, ResubmitableObjectMixin
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, 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
@@ -75,7 +80,7 @@ class AbstractCompensation(BaseObject,
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):
@@ -199,7 +204,9 @@ class AbstractCompensation(BaseObject,
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
@@ -296,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
@@ -328,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
@@ -379,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:
@@ -388,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
@@ -401,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
@@ -459,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):
""" """
@@ -493,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,12 +9,13 @@ 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
@@ -52,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:
@@ -96,15 +92,9 @@ 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))
def get_state_after_surface_sum(self) -> float: return val
""" 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 __calculate_deductable_rest(self): def __calculate_deductable_rest(self):
""" Calculates available rest surface of the eco account """ Calculates available rest surface of the eco account
@@ -114,10 +104,7 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
Returns: Returns:
ret_val_total (float): Total amount ret_val_total (float): Total amount
""" """
deductions = self.deductions.filter( deductions_surfaces = self.get_deductions_surface()
intervention__deleted=None,
)
deductions_surfaces = deductions.aggregate(Sum("surface"))["surface__sum"] or 0
available_surface = self.deductable_surface available_surface = self.deductable_surface
if available_surface is None: if available_surface is None:
@@ -181,12 +168,12 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
# Send mail # Send mail
shared_users = self.shared_users.values_list("id", flat=True) shared_users = self.shared_users.values_list("id", flat=True)
for user_id in shared_users: for user_id in shared_users:
celery_send_mail_deduction_changed.delay(self.identifier, self.title, user_id, data_change) celery_send_mail_deduction_changed.delay(self.id, self.get_app_object_tuple(), user_id, data_change)
# Send mail # Send mail
shared_teams = self.shared_teams.values_list("id", flat=True) shared_teams = self.shared_teams.values_list("id", flat=True)
for team_id in shared_teams: for team_id in shared_teams:
celery_send_mail_deduction_changed_team.delay(self.identifier, self.title, team_id, data_change) celery_send_mail_deduction_changed_team.delay(self.id, self.get_app_object_tuple(), team_id, data_change)
def update_deductable_rest(self): def update_deductable_rest(self):
""" """
@@ -211,6 +198,19 @@ class EcoAccount(AbstractCompensation, ShareableObjectMixin, RecordableObjectMix
ret_val = 0 ret_val = 0
return ret_val 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):
""" """
@@ -245,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))

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

@@ -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

@@ -8,6 +8,7 @@ Created on: 18.08.22
from django.http import HttpRequest from django.http import HttpRequest
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.urls import reverse from django.urls import reverse
from django.utils.formats import number_format
from django.utils.html import format_html from django.utils.html import format_html
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@@ -111,6 +112,7 @@ class EcoAccountTable(BaseTable, TableRenderMixin, TableOrderMixin):
except ZeroDivisionError: except ZeroDivisionError:
value_relative = 0 value_relative = 0
html = render_to_string("konova/widgets/progressbar.html", {"value": value_relative}) 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) return format_html(html)
def render_r(self, value, record: EcoAccount): def render_r(self, value, record: EcoAccount):

View File

@@ -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>

View File

@@ -25,7 +25,7 @@
{% trans 'Missing finished deadline ' %} {% trans 'Missing finished deadline ' %}
</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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -130,7 +130,7 @@
{% else %} {% else %}
<span title="{% trans 'The data must be shared with you, if you want to see which other users have shared access as well.' %}"> <span title="{% trans 'The data must be shared with you, if you want to see which other users have shared access as well.' %}">
{% fa5_icon 'eye-slash' %} {% fa5_icon 'eye-slash' %}
{{obj.users.count}} {% trans 'other users' %} {{obj.intervention.users.count}} {% trans 'other users' %}
</span> </span>
{% endif %} {% endif %}
</td> </td>

View 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>

View File

@@ -25,7 +25,7 @@
{% trans 'Missing finished deadline ' %} {% trans 'Missing finished deadline ' %}
</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>

View 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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -125,10 +125,16 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.compensation = self.fill_out_compensation(self.compensation) self.compensation = self.fill_out_compensation(self.compensation)
pre_edit_log_count = self.compensation.log.count() pre_edit_log_count = self.compensation.log.count()
self.assertTrue(self.compensation.is_shared_with(self.superuser))
old_identifier = self.compensation.identifier
new_title = self.create_dummy_string() new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string() new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string() new_comment = self.create_dummy_string()
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry new_geometry = MultiPolygon(
self.compensation.geometry.geom.buffer(10),
srid=self.compensation.geometry.geom.srid
) # Create a geometry which differs from the stored one
geojson = self.create_geojson(new_geometry) geojson = self.create_geojson(new_geometry)
check_on_elements = { check_on_elements = {
@@ -151,19 +157,21 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
check_on_elements = { check_on_elements = {
self.compensation.title: new_title, self.compensation.title: new_title,
self.compensation.identifier: new_identifier,
self.compensation.comment: new_comment, self.compensation.comment: new_comment,
} }
for k, v in check_on_elements.items(): for k, v in check_on_elements.items():
self.assertEqual(k, v) self.assertEqual(k, v)
self.assert_equal_geometries(self.compensation.geometry.geom, new_geometry) # Expect identifier to not be editable
self.assertEqual(self.compensation.identifier, old_identifier, msg="Identifier was editable!")
# Expect logs to be set # Expect logs to be set
self.assertEqual(pre_edit_log_count + 1, self.compensation.log.count()) self.assertEqual(pre_edit_log_count + 1, self.compensation.log.count())
self.assertEqual(self.compensation.log.first().action, UserAction.EDITED) self.assertEqual(self.compensation.log.first().action, UserAction.EDITED)
self.assert_equal_geometries(self.compensation.geometry.geom, new_geometry)
def test_checkability(self): def test_checkability(self):
""" """
This tests if the checkability of the compensation (which is defined by the linked intervention's checked This tests if the checkability of the compensation (which is defined by the linked intervention's checked
@@ -244,6 +252,7 @@ class CompensationWorkflowTestCase(BaseWorkflowTestCase):
self.client_user.post(record_url, post_data) self.client_user.post(record_url, post_data)
# Check that the intervention is still not recorded # Check that the intervention is still not recorded
self.intervention.refresh_from_db()
self.assertIsNone(self.intervention.recorded) self.assertIsNone(self.intervention.recorded)
# Now fill out the data for a compensation # Now fill out the data for a compensation

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: 21.08.23
"""

View File

@@ -0,0 +1,318 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 21.08.23
"""
from django.core.exceptions import ObjectDoesNotExist
from django.test import RequestFactory
from django.utils.translation import gettext_lazy as _
from codelist.models import KonovaCodeList
from codelist.settings import CODELIST_COMPENSATION_ACTION_ID, CODELIST_BIOTOPES_ID
from compensation.forms.modals.compensation_action import NewCompensationActionModalForm, \
EditCompensationActionModalForm, RemoveCompensationActionModalForm
from compensation.forms.modals.state import NewCompensationStateModalForm, EditCompensationStateModalForm, \
RemoveCompensationStateModalForm
from compensation.models import UnitChoices
from konova.tests.test_views import BaseTestCase
from konova.utils.generators import generate_random_string
from konova.utils.message_templates import COMPENSATION_ACTION_EDITED, ADDED_COMPENSATION_ACTION, \
COMPENSATION_ACTION_REMOVED, ADDED_COMPENSATION_STATE, COMPENSATION_STATE_EDITED, \
COMPENSATION_STATE_REMOVED
from user.models import UserAction
class NewCompensationActionModalFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.superuser
self.action_dummy_code = self.create_dummy_codes().first()
action_list = KonovaCodeList.objects.get_or_create(
id=CODELIST_COMPENSATION_ACTION_ID,
)[0]
action_list.codes.add(self.action_dummy_code)
def test_init(self):
form = NewCompensationActionModalForm()
self.assertEqual(form.form_title, str(_("New action")))
self.assertEqual(form.form_caption, str(_("Insert data for the new action")))
self.assertTrue(len(form.fields["action_type"].choices) == 1)
def test_save(self):
comment = "TEST_comment"
unit = UnitChoices.km
amount = 2.5
data = {
"action_type": [self.action_dummy_code.id],
"action_type_details": [],
"unit": unit,
"amount": amount,
"comment": comment,
}
form = NewCompensationActionModalForm(data, request=self.request, instance=self.compensation)
self.assertTrue(form.is_valid(), msg=form.errors)
comp_action = form.save()
last_log = self.compensation.log.first()
self.assertIn(comp_action, self.compensation.actions.all())
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, ADDED_COMPENSATION_ACTION)
self.assertEqual(comp_action.amount, amount)
self.assertEqual(comp_action.unit, unit)
self.assertEqual(comp_action.comment, comment)
comp_action_types = comp_action.action_type.all()
self.assertEqual(comp_action_types.count(), 1)
self.assertEqual(comp_action_types.first(), self.action_dummy_code)
class EditCompensationActionModalFormTestCase(NewCompensationActionModalFormTestCase):
def setUp(self) -> None:
super().setUp()
self.comp_action = self.create_dummy_action()
self.compensation.actions.add(self.comp_action)
def test_init(self):
form = EditCompensationActionModalForm(request=self.request, instance=self.compensation, action=self.comp_action)
self.assertEqual(form.form_title, str(_("Edit action")))
self.assertEqual(len(form.fields["action_type"].initial), self.comp_action.action_type.count())
self.assertEqual(len(form.fields["action_type_details"].initial), self.comp_action.action_type_details.count())
self.assertEqual(form.fields["amount"].initial, self.comp_action.amount)
self.assertEqual(form.fields["unit"].initial, self.comp_action.unit)
self.assertEqual(form.fields["comment"].initial, self.comp_action.comment)
def test_save(self):
amount = 25.4
unit = UnitChoices.cm
comment = generate_random_string(length=20, use_numbers=True, use_letters_lc=True, use_letters_uc=True)
data = {
"action_type": [self.action_dummy_code.id],
"action_type_details": [],
"amount": amount,
"unit": unit,
"comment": comment,
}
form = EditCompensationActionModalForm(data, request=self.request, instance=self.compensation, action=self.comp_action)
self.assertTrue(form.is_valid())
action = form.save()
self.assertEqual(action.action_type.count(), len(data["action_type"]))
self.assertEqual(action.action_type_details.count(), 0)
self.assertEqual(float(action.amount), amount)
self.assertEqual(action.unit, unit)
self.assertEqual(action.comment, comment)
last_log = self.compensation.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, COMPENSATION_ACTION_EDITED)
self.assertIn(action, self.compensation.actions.all())
self.assertEqual(self.compensation.actions.count(), 1)
class RemoveCompensationActionModalFormTestCase(EditCompensationActionModalFormTestCase):
def setUp(self) -> None:
super().setUp()
def test_init(self):
self.assertIn(self.comp_action, self.compensation.actions.all())
form = RemoveCompensationActionModalForm(request=self.request, instance=self.compensation, action=self.comp_action)
self.assertEqual(form.action, self.comp_action)
def test_save(self):
data = {
"confirm": True,
}
form = RemoveCompensationActionModalForm(
data,
request=self.request,
instance=self.compensation,
action=self.comp_action
)
self.assertTrue(form.is_valid())
self.assertIn(self.comp_action, self.compensation.actions.all())
form.save()
last_log = self.compensation.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, COMPENSATION_ACTION_REMOVED)
self.assertNotIn(self.comp_action, self.compensation.actions.all())
try:
self.comp_action.refresh_from_db()
self.fail(msg="This action should not be fetchable anymore")
except ObjectDoesNotExist:
pass
class NewCompensationStateModalFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.superuser
self.comp_biotope_code = self.create_dummy_codes().first()
self.biotope_codelist = KonovaCodeList.objects.get_or_create(
id=CODELIST_BIOTOPES_ID
)[0]
self.biotope_codelist.codes.add(self.comp_biotope_code)
def test_init(self):
form = NewCompensationStateModalForm(request=self.request, instance=self.compensation)
self.assertEqual(form.form_title, str(_("New state")))
self.assertEqual(form.form_caption, str(_("Insert data for the new state")))
self.assertEqual(len(form.fields["biotope_type"].choices), 1)
def test_save(self):
test_surface = 123.45
data = {
"biotope_type": self.comp_biotope_code.id,
"biotope_extra": [],
"surface": test_surface,
}
self.assertEqual(self.compensation.before_states.count(), 0)
self.assertEqual(self.compensation.after_states.count(), 0)
form = NewCompensationStateModalForm(data, request=self.request, instance=self.compensation)
self.assertTrue(form.is_valid(), msg=form.errors)
is_before_state = True
state = form.save(is_before_state)
self.assertEqual(self.compensation.before_states.count(), 1)
self.assertEqual(self.compensation.after_states.count(), 0)
self.assertIn(state, self.compensation.before_states.all())
self.assertEqual(state.biotope_type, self.comp_biotope_code)
self.assertEqual(state.biotope_type_details.count(), 0)
self.assertEqual(float(state.surface), test_surface)
last_log = self.compensation.log.first()
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, ADDED_COMPENSATION_STATE)
is_before_state = False
state = form.save(is_before_state)
self.assertEqual(self.compensation.before_states.count(), 1)
self.assertEqual(self.compensation.after_states.count(), 1)
self.assertIn(state, self.compensation.after_states.all())
self.assertEqual(state.biotope_type, self.comp_biotope_code)
self.assertEqual(state.biotope_type_details.count(), 0)
self.assertEqual(float(state.surface), test_surface)
last_log = self.compensation.log.first()
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, ADDED_COMPENSATION_STATE)
class EditCompensationStateModalFormTestCase(NewCompensationStateModalFormTestCase):
def setUp(self) -> None:
super().setUp()
self.comp_state.biotope_type = self.comp_biotope_code
self.comp_state.save()
self.compensation.after_states.add(self.comp_state)
def test_init(self):
form = EditCompensationStateModalForm(request=self.request, instance=self.compensation, state=self.comp_state)
self.assertEqual(form.state, self.comp_state)
self.assertEqual(form.form_title, str(_("Edit state")))
self.assertEqual(form.fields["biotope_type"].initial, self.comp_state.biotope_type.id)
self.assertTrue(
form.fields["biotope_extra"].initial.difference(
self.comp_state.biotope_type_details.all()
).count() == 0
)
self.assertEqual(form.fields["surface"].initial, self.comp_state.surface)
def test_save(self):
test_surface = 987.65
test_code = self.create_dummy_codes().exclude(
id=self.comp_biotope_code.id
).first()
self.biotope_codelist.codes.add(test_code)
self.assertEqual(self.compensation.after_states.count(), 1)
self.assertEqual(self.compensation.before_states.count(), 0)
data = {
"biotope_type": test_code.id,
"biotope_extra": [],
"surface": test_surface,
}
form = EditCompensationStateModalForm(
data,
request=self.request,
instance=self.compensation,
state=self.comp_state
)
self.assertTrue(form.is_valid(), msg=form.errors)
is_before_state = False
state = form.save(is_before_state=is_before_state)
self.assertEqual(state.biotope_type, test_code)
self.assertEqual(state.biotope_type_details.count(), 0)
self.assertEqual(float(state.surface), test_surface)
last_log = self.compensation.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, COMPENSATION_STATE_EDITED)
class RemoveCompensationStateModalFormTestCase(EditCompensationStateModalFormTestCase):
def setUp(self) -> None:
super().setUp()
def test_init(self):
form = RemoveCompensationStateModalForm(request=self.request, instance=self.compensation, state=self.comp_state)
self.assertEqual(form.state, self.comp_state)
def test_save(self):
data = {
"confirm": True
}
form = RemoveCompensationStateModalForm(
data,
request=self.request,
instance=self.compensation,
state=self.comp_state
)
self.assertTrue(form.is_valid(), msg=form.errors)
self.assertIn(self.comp_state, self.compensation.after_states.all())
self.assertNotIn(self.comp_state, self.compensation.before_states.all())
form.save()
self.assertEqual(self.compensation.after_states.count(), 0)
self.assertEqual(self.compensation.before_states.count(), 0)
try:
self.comp_state.refresh_from_db()
self.fail("Entry should not existing anymore")
except ObjectDoesNotExist:
pass
last_log = self.compensation.log.first()
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, COMPENSATION_STATE_REMOVED)

View File

@@ -0,0 +1,201 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 30.08.23
"""
from django.core.exceptions import ObjectDoesNotExist
from django.test import RequestFactory
from django.utils.timezone import now
from compensation.forms.modals.deadline import NewDeadlineModalForm
from compensation.models import CompensationDocument
from konova.forms.modals import RemoveDeadlineModalForm
from konova.models import DeadlineType
from konova.tests.test_views import BaseTestCase
from konova.utils.message_templates import DEADLINE_REMOVED, DOCUMENT_REMOVED_TEMPLATE, COMPENSATION_REMOVED_TEMPLATE, \
DEADLINE_ADDED
from user.models import UserAction, Team
class AbstractCompensationModelTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.request = RequestFactory().request()
self.request.user = self.superuser
def test_remove_deadline(self):
self.compensation.deadlines.add(self.finished_deadline)
data = {
"confirm": True
}
form = RemoveDeadlineModalForm(
data,
request=self.request,
instance=self.compensation,
deadline=self.finished_deadline,
)
self.assertTrue(form.is_valid(), msg=form.errors)
self.assertIn(self.finished_deadline, self.compensation.deadlines.all())
form.save()
last_log = self.compensation.log.first()
self.assertEqual(last_log.user, self.request.user)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, DEADLINE_REMOVED)
self.assertNotIn(self.finished_deadline, self.compensation.deadlines.all())
try:
self.finished_deadline.refresh_from_db()
self.fail("Deadline should not exist anymore after removing from abstract compensation")
except ObjectDoesNotExist:
pass
def test_add_deadline(self):
request = RequestFactory().request()
request.user = self.superuser
data = {
"type": DeadlineType.MAINTAIN,
"date": now().date(),
"comment": "TestDeadline"
}
form = NewDeadlineModalForm(
data,
request=self.request,
instance=self.compensation,
)
self.assertTrue(form.is_valid(), msg=form.errors)
deadline = self.compensation.add_deadline(form)
self.assertEqual(deadline.date, data["date"])
self.assertEqual(deadline.type, data["type"])
self.assertEqual(deadline.comment, data["comment"])
self.assertEqual(deadline.created.action, UserAction.CREATED)
self.assertEqual(deadline.created.user, self.superuser)
self.assertEqual(deadline.created.comment, None)
self.assertIn(deadline, self.compensation.deadlines.all())
last_log = self.compensation.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, DEADLINE_ADDED)
class CompensationTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_str(self):
self.assertEqual(str(self.compensation), self.compensation.identifier)
def test_save(self):
old_identifier = self.compensation.identifier
self.compensation.identifier = None
self.compensation.save()
self.assertIsNotNone(self.compensation.identifier)
self.assertNotEqual(old_identifier, self.compensation.identifier)
def test_share_with_user(self):
self.assertNotIn(self.user, self.compensation.shared_users)
self.compensation.share_with_user(self.user)
self.assertIn(self.user, self.compensation.shared_users)
def test_share_with_user_list(self):
user_list = [
self.user
]
self.assertNotIn(self.user, self.compensation.shared_users)
self.compensation.share_with_user_list(user_list)
self.assertIn(self.user, self.compensation.shared_users)
user_list = [
self.superuser
]
self.assertNotIn(self.superuser, self.compensation.shared_users)
self.compensation.share_with_user_list(user_list)
self.assertIn(self.superuser, self.compensation.shared_users)
self.assertNotIn(self.user, self.compensation.shared_users)
def test_share_with_team(self):
self.assertNotIn(self.team, self.compensation.shared_teams)
self.compensation.share_with_team(self.team)
self.assertIn(self.team, self.compensation.shared_teams)
def test_share_with_team_list(self):
self.compensation.share_with_team(self.team)
self.assertIn(self.team, self.compensation.shared_teams)
other_team = Team.objects.create(
name="NewTeam"
)
team_list = [
other_team
]
self.compensation.share_with_team_list(team_list)
self.assertIn(other_team, self.compensation.shared_teams)
self.assertNotIn(self.team, self.compensation.shared_teams)
def test_shared_users(self):
intervention = self.compensation.intervention
diff = self.compensation.shared_users.difference(intervention.shared_users)
self.assertEqual(diff.count(), 0)
self.compensation.share_with_user(self.superuser)
diff = self.compensation.shared_users.difference(intervention.shared_users)
self.assertEqual(diff.count(), 0)
def test_shared_teams(self):
intervention = self.compensation.intervention
diff = self.compensation.shared_users.difference(intervention.shared_users)
self.assertEqual(diff.count(), 0)
self.compensation.share_with_user(self.superuser)
diff = self.compensation.shared_users.difference(intervention.shared_users)
self.assertEqual(diff.count(), 0)
def test_get_documents(self):
doc = self.create_dummy_document(CompensationDocument, self.compensation)
docs = self.compensation.get_documents()
self.assertIn(doc, docs)
def test_mark_as_deleted(self):
self.assertIsNone(self.compensation.deleted)
self.compensation.mark_as_deleted(self.superuser, send_mail=False)
comp_deleted = self.compensation.deleted
self.assertIsNotNone(comp_deleted)
self.assertEqual(comp_deleted.action, UserAction.DELETED)
self.assertEqual(comp_deleted.user, self.superuser)
self.assertEqual(comp_deleted.comment, None)
intervention_last_log = self.compensation.intervention.log.first()
self.assertEqual(intervention_last_log.action, UserAction.EDITED)
self.assertEqual(intervention_last_log.user, self.superuser)
self.assertEqual(
intervention_last_log.comment,
COMPENSATION_REMOVED_TEMPLATE.format(
self.compensation.identifier
)
)
class CompensationDocumentTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
self.doc = self.create_dummy_document(CompensationDocument, self.compensation)
def test_delete(self):
doc_title = self.doc.title
self.assertIn(self.doc, self.compensation.get_documents())
self.doc.delete(self.superuser)
self.assertNotIn(self.doc, self.compensation.get_documents())
try:
self.doc.refresh_from_db()
self.fail("Document should not be fetchable anymore")
except ObjectDoesNotExist:
pass
last_log = self.compensation.log.first()
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, DOCUMENT_REMOVED_TEMPLATE.format(doc_title))

View File

@@ -82,6 +82,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
url = reverse("compensation:acc:edit", args=(self.eco_account.id,)) url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
pre_edit_log_count = self.eco_account.log.count() pre_edit_log_count = self.eco_account.log.count()
old_identifier = self.eco_account.identifier
new_title = self.create_dummy_string() new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string() new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string() new_comment = self.create_dummy_string()
@@ -106,20 +107,23 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
"surface": test_deductable_surface, "surface": test_deductable_surface,
"conservation_office": test_conservation_office.id "conservation_office": test_conservation_office.id
} }
self.client_user.post(url, post_data) response = self.client_user.post(url, post_data)
self.assertEqual(response.status_code, 302, msg=f"{response.content.decode('utf-8')}")
self.eco_account.refresh_from_db() self.eco_account.refresh_from_db()
deductions_surface = self.eco_account.get_deductions_surface()
check_on_elements = { check_on_elements = {
self.eco_account.title: new_title, self.eco_account.title: new_title,
self.eco_account.identifier: new_identifier,
self.eco_account.deductable_surface: test_deductable_surface, self.eco_account.deductable_surface: test_deductable_surface,
self.eco_account.deductable_rest: test_deductable_surface, self.eco_account.deductable_rest: test_deductable_surface - deductions_surface,
self.eco_account.comment: new_comment, self.eco_account.comment: new_comment,
} }
for k, v in check_on_elements.items(): for k, v in check_on_elements.items():
self.assertEqual(k, v) self.assertEqual(k, v)
self.assertEqual(self.eco_account.identifier, old_identifier)
self.assert_equal_geometries(self.eco_account.geometry.geom, new_geometry) self.assert_equal_geometries(self.eco_account.geometry.geom, new_geometry)
# Expect logs to be set # Expect logs to be set
@@ -223,7 +227,9 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
self.eco_account.refresh_from_db() self.eco_account.refresh_from_db()
self.assertEqual(1, self.eco_account.deductions.count()) self.assertEqual(1, self.eco_account.deductions.count())
self.assertEqual(1, self.intervention.deductions.count()) self.assertEqual(1, self.intervention.deductions.count())
deduction = self.eco_account.deductions.first() deduction = self.eco_account.deductions.get(
surface=test_surface
)
self.assertEqual(deduction.surface, test_surface) self.assertEqual(deduction.surface, test_surface)
self.assertEqual(self.eco_account.deductable_rest, self.eco_account.deductable_surface - deduction.surface) self.assertEqual(self.eco_account.deductable_rest, self.eco_account.deductable_surface - deduction.surface)
self.assertEqual(deduction.account, self.eco_account) self.assertEqual(deduction.account, self.eco_account)

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: 30.08.23
"""

View File

@@ -0,0 +1,128 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 30.08.23
"""
from django.core.exceptions import ObjectDoesNotExist
from django.urls import reverse
from compensation.models import EcoAccountDocument
from konova.tests.test_views import BaseTestCase
from konova.utils.message_templates import DOCUMENT_REMOVED_TEMPLATE, DEDUCTION_REMOVED
from user.models import UserAction
class EcoAccountTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_str(self):
self.assertEqual(str(self.eco_account), f"{self.eco_account.identifier} ({self.eco_account.title})")
def test_save(self):
old_id = self.eco_account.identifier
self.assertIsNotNone(self.eco_account.identifier)
self.eco_account.identifier = None
self.eco_account.save()
self.assertIsNotNone(self.eco_account.identifier)
self.assertNotEqual(old_id, self.eco_account.identifier)
def test_property_deductions_surface_sum(self):
self.assertEqual(
self.eco_account.deductions_surface_sum,
self.eco_account.get_deductions_surface()
)
def test_get_documents(self):
docs = self.eco_account.get_documents()
self.assertEqual(docs.count(), 0)
doc = self.create_dummy_document(EcoAccountDocument, self.eco_account)
self.assertIn(doc, self.eco_account.get_documents())
def test_get_share_link(self):
self.assertEqual(
self.eco_account.get_share_link(),
reverse(
"compensation:acc:share-token",
args=(self.eco_account.id, self.eco_account.access_token)
)
)
def test_get_deductable_rest_relative(self):
self.assertEqual(self.eco_account.deductions.count(), 0)
self.eco_account.deductable_surface = 5.0
self.eco_account.save()
self.eco_account.update_deductable_rest()
self.assertEqual(self.eco_account.get_deductable_rest_relative(), 100)
self.eco_account.deductable_surface = None
self.eco_account.save()
self.assertEqual(self.eco_account.get_deductable_rest_relative(), 0)
class EcoAccountDocumentTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_delete(self):
doc = self.create_dummy_document(
EcoAccountDocument,
self.eco_account
)
doc_title = doc.title
docs = self.eco_account.get_documents()
self.assertIn(doc, docs)
doc.delete(user=self.superuser)
last_log = self.eco_account.log.first()
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.comment, DOCUMENT_REMOVED_TEMPLATE.format(
doc_title
))
try:
doc.refresh_from_db()
self.fail("Document should not have been fetchable")
except ObjectDoesNotExist:
pass
class EcoAccountDeductionTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_str(self):
self.assertEqual(str(self.deduction), f"{self.deduction.surface} of {self.deduction.account}")
def test_delete(self):
self.deduction.account = self.eco_account
self.deduction.intervention = self.intervention
self.deduction.save()
self.eco_account.update_deductable_rest()
old_deductable_rest = self.eco_account.deductable_rest
deduction_surface = self.deduction.surface
self.deduction.delete(self.superuser)
last_log_intervention = self.intervention.log.first()
last_log_account = self.eco_account.log.first()
logs = [
last_log_intervention,
last_log_account,
]
for log in logs:
self.assertEqual(log.action, UserAction.EDITED)
self.assertEqual(log.user, self.superuser)
self.assertEqual(log.comment, DEDUCTION_REMOVED)
self.assertLess(old_deductable_rest, self.eco_account.deductable_rest)
self.assertEqual(old_deductable_rest + deduction_surface, self.eco_account.deductable_rest)
try:
self.deduction.refresh_from_db()
self.fail("Deduction still fetchable after deleting")
except ObjectDoesNotExist:
pass

View File

@@ -30,6 +30,7 @@ class CompensationQualityChecker(AbstractQualityChecker):
""" """
after_states = self.obj.get_surface_after_states() after_states = self.obj.get_surface_after_states()
before_states = self.obj.get_surface_before_states() before_states = self.obj.get_surface_before_states()
if after_states != before_states: if after_states != before_states:
self.messages.append( self.messages.append(
_("States unequal") _("States unequal")
@@ -90,10 +91,11 @@ class EcoAccountQualityChecker(CompensationQualityChecker):
Returns: Returns:
""" """
surface = self.obj.deductable_surface surface = self.obj.deductable_surface or 0
if surface is None or surface == 0: is_surface_invalid = surface == 0
if is_surface_invalid:
self._add_missing_attr_name(_("Available Surface")) self._add_missing_attr_name(_("Available Surface"))
after_state_surface = self.obj.get_state_after_surface_sum() after_state_surface = self.obj.get_surface_after_states()
if surface > after_state_surface: if surface > after_state_surface:
self.messages.append( self.messages.append(
_("Deductable surface can not be larger than state surface") _("Deductable surface can not be larger than state surface")

View File

@@ -19,15 +19,15 @@ from compensation.models import Compensation
from compensation.tables.compensation import CompensationTable from compensation.tables.compensation import CompensationTable
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \ from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
RECORDED_BLOCKS_EDIT, CHECKED_RECORDED_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \ RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \
COMPENSATION_ADDED_TEMPLATE COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
from konova.utils.user_checks import in_group
@login_required @login_required
@@ -103,6 +103,11 @@ def new_view(request: HttpRequest, intervention_id: str = None):
) )
) )
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier)) messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
if geom_form.geometry_simplified:
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
return redirect("compensation:detail", id=comp.id) return redirect("compensation:detail", id=comp.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -165,16 +170,20 @@ def edit_view(request: HttpRequest, id: str):
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp) geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=comp)
if request.method == "POST": if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid(): if data_form.is_valid() and geom_form.is_valid():
# Preserve state of intervention recorded/checked to determine whether the user must be informed or not # Preserve state of intervention checked to determine whether the user must be informed or not
# about a change of the recorded/checked state # about a change of the check state
intervention_recorded = comp.intervention.recorded is not None intervention_is_checked = comp.intervention.checked is not None
intervention_checked = comp.intervention.checked is not None
# The data form takes the geom form for processing, as well as the performing user # The data form takes the geom form for processing, as well as the performing user
comp = data_form.save(request.user, geom_form) comp = data_form.save(request.user, geom_form)
if intervention_recorded or intervention_checked: if intervention_is_checked:
messages.info(request, CHECKED_RECORDED_RESET) messages.info(request, CHECK_STATE_RESET)
messages.success(request, _("Compensation {} edited").format(comp.identifier)) messages.success(request, _("Compensation {} edited").format(comp.identifier))
if geom_form.geometry_simplified:
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
return redirect("compensation:detail", id=comp.id) return redirect("compensation:detail", id=comp.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -192,6 +201,7 @@ def edit_view(request: HttpRequest, id: str):
@login_required @login_required
@any_group_check @any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str): def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation """ Renders a detail view for a compensation
@@ -203,7 +213,16 @@ def detail_view(request: HttpRequest, id: str):
""" """
template = "compensation/detail/compensation/view.html" template = "compensation/detail/compensation/view.html"
comp = get_object_or_404(Compensation, id=id) comp = get_object_or_404(
Compensation.objects.select_related(
"modified",
"created",
"geometry"
),
id=id,
deleted=None,
intervention__deleted=None,
)
geom_form = SimpleGeomForm(instance=comp) geom_form = SimpleGeomForm(instance=comp)
parcels = comp.get_underlying_parcels() parcels = comp.get_underlying_parcels()
_user = request.user _user = request.user
@@ -216,8 +235,8 @@ def detail_view(request: HttpRequest, id: str):
# Precalculate logical errors between before- and after-states # Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0 sum_before_states = comp.get_surface_before_states()
sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 sum_after_states = comp.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states) diff_states = abs(sum_before_states - sum_after_states)
request = comp.set_status_messages(request) request = comp.set_status_messages(request)
@@ -227,6 +246,13 @@ def detail_view(request: HttpRequest, id: str):
if last_checked: if last_checked:
last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user) last_checked_tooltip = DATA_CHECKED_PREVIOUSLY_TEMPLATE.format(last_checked.get_timestamp_str_formatted(), last_checked.user)
requesting_user_is_only_shared_user = comp.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = { context = {
"obj": comp, "obj": comp,
"last_checked": last_checked, "last_checked": last_checked,
@@ -240,9 +266,9 @@ def detail_view(request: HttpRequest, id: str):
"sum_before_states": sum_before_states, "sum_before_states": sum_before_states,
"sum_after_states": sum_after_states, "sum_after_states": sum_after_states,
"diff_states": diff_states, "diff_states": diff_states,
"is_default_member": in_group(_user, DEFAULT_GROUP), "is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP), "is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP), "is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": comp.get_LANIS_link(), "LANIS_LINK": comp.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}", TAB_TITLE_IDENTIFIER: f"{comp.identifier} - {comp.title}",
"has_finished_deadlines": comp.get_finished_deadlines().exists(), "has_finished_deadlines": comp.get_finished_deadlines().exists(),

View File

@@ -73,6 +73,7 @@ def report_view(request: HttpRequest, id: str):
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels, "parcels": parcels,
"actions": actions, "actions": actions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context

View File

@@ -17,13 +17,13 @@ from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm
from compensation.models import EcoAccount from compensation.models import EcoAccount
from compensation.tables.eco_account import EcoAccountTable from compensation.tables.eco_account import EcoAccountTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \ from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
IDENTIFIER_REPLACED IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
from konova.utils.user_checks import in_group
@login_required @login_required
@@ -84,6 +84,11 @@ def new_view(request: HttpRequest):
) )
) )
messages.success(request, _("Eco-Account {} added").format(acc.identifier)) messages.success(request, _("Eco-Account {} added").format(acc.identifier))
if geom_form.geometry_simplified:
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
return redirect("compensation:acc:detail", id=acc.id) return redirect("compensation:acc:detail", id=acc.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -145,10 +150,17 @@ def edit_view(request: HttpRequest, id: str):
data_form = EditEcoAccountForm(request.POST or None, instance=acc) data_form = EditEcoAccountForm(request.POST or None, instance=acc)
geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc) geom_form = SimpleGeomForm(request.POST or None, read_only=False, instance=acc)
if request.method == "POST": if request.method == "POST":
if data_form.is_valid() and geom_form.is_valid(): data_form_valid = data_form.is_valid()
geom_form_valid = geom_form.is_valid()
if data_form_valid and geom_form_valid:
# The data form takes the geom form for processing, as well as the performing user # The data form takes the geom form for processing, as well as the performing user
acc = data_form.save(request.user, geom_form) acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier)) messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
if geom_form.geometry_simplified:
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
return redirect("compensation:acc:detail", id=acc.id) return redirect("compensation:acc:detail", id=acc.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -166,6 +178,7 @@ def edit_view(request: HttpRequest, id: str):
@login_required @login_required
@any_group_check @any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str): def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation """ Renders a detail view for a compensation
@@ -184,7 +197,8 @@ def detail_view(request: HttpRequest, id: str):
'geometry', 'geometry',
'responsible', 'responsible',
), ),
id=id id=id,
deleted=None,
) )
geom_form = SimpleGeomForm(instance=acc) geom_form = SimpleGeomForm(instance=acc)
parcels = acc.get_underlying_parcels() parcels = acc.get_underlying_parcels()
@@ -197,8 +211,8 @@ def detail_view(request: HttpRequest, id: str):
# Precalculate logical errors between before- and after-states # Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0 sum_before_states = acc.get_surface_before_states()
sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 sum_after_states = acc.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states) diff_states = abs(sum_before_states - sum_after_states)
# Calculate rest of available surface for deductions # Calculate rest of available surface for deductions
available_total = acc.deductable_rest available_total = acc.deductable_rest
@@ -212,6 +226,13 @@ def detail_view(request: HttpRequest, id: str):
request = acc.set_status_messages(request) request = acc.set_status_messages(request)
requesting_user_is_only_shared_user = acc.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = { context = {
"obj": acc, "obj": acc,
"geom_form": geom_form, "geom_form": geom_form,
@@ -224,9 +245,9 @@ def detail_view(request: HttpRequest, id: str):
"diff_states": diff_states, "diff_states": diff_states,
"available": available_relative, "available": available_relative,
"available_total": available_total, "available_total": available_total,
"is_default_member": in_group(_user, DEFAULT_GROUP), "is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP), "is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP), "is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": acc.get_LANIS_link(), "LANIS_LINK": acc.get_LANIS_link(),
"deductions": deductions, "deductions": deductions,
"actions": actions, "actions": actions,
@@ -257,7 +278,7 @@ def remove_view(request: HttpRequest, id: str):
# default group user # default group user
if acc.recorded is not None or acc.deductions.exists(): if acc.recorded is not None or acc.deductions.exists():
user = request.user user = request.user
if not in_group(user, ETS_GROUP): if not user.in_group(ETS_GROUP):
messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED) messages.info(request, CANCEL_ACC_RECORDED_OR_DEDUCTED)
return redirect("compensation:acc:detail", id=id) return redirect("compensation:acc:detail", id=id)

View File

@@ -80,6 +80,7 @@ def report_view(request: HttpRequest, id: str):
"parcels": parcels, "parcels": parcels,
"actions": actions, "actions": actions,
"deductions": deductions, "deductions": deductions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context

View File

@@ -16,4 +16,5 @@ class EmaAdmin(AbstractCompensationAdmin):
"teams", "teams",
] ]
admin.site.register(Ema, EmaAdmin) admin.site.register(Ema, EmaAdmin)

View File

@@ -76,7 +76,7 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
) )
# Finally create main object # Finally create main object
acc = Ema.objects.create( ema = Ema.objects.create(
identifier=identifier, identifier=identifier,
title=title, title=title,
responsible=responsible, responsible=responsible,
@@ -87,16 +87,16 @@ class NewEmaForm(AbstractCompensationForm, CompensationResponsibleFormMixin, Pik
) )
# Add the creating user to the list of shared users # Add the creating user to the list of shared users
acc.share_with_user(user) ema.share_with_user(user)
# Add the log entry to the main objects log list # Add the log entry to the main objects log list
acc.log.add(action) ema.log.add(action)
# Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!) # Process the geometry form (NOT ATOMIC TRANSACTION DUE TO CELERY!)
geometry = geom_form.save(action) geometry = geom_form.save(action)
acc.geometry = geometry ema.geometry = geometry
acc.save() ema.save()
return acc return ema
class EditEmaForm(NewEmaForm): class EditEmaForm(NewEmaForm):
@@ -133,7 +133,6 @@ class EditEmaForm(NewEmaForm):
def save(self, user: User, geom_form: SimpleGeomForm): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
# Fetch data from cleaned POST values # Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
handler_type = self.cleaned_data.get("handler_type", None) handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None) handler_detail = self.cleaned_data.get("handler_detail", None)
@@ -154,7 +153,6 @@ class EditEmaForm(NewEmaForm):
self.instance.responsible.save() self.instance.responsible.save()
# Update main oject data # Update main oject data
self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.comment = comment self.instance.comment = comment
self.instance.is_pik = is_pik self.instance.is_pik = is_pik

View File

@@ -0,0 +1,35 @@
# Generated by Django 4.2.6 on 2023-11-30 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('compensation', '0015_alter_compensation_after_states_and_more'),
('konova', '0014_resubmission'),
('ema', '0008_auto_20221116_1322'),
]
operations = [
migrations.AlterField(
model_name='ema',
name='after_states',
field=models.ManyToManyField(blank=True, help_text="Refers to 'Zielzustand Biotop'", related_name='+', to='compensation.compensationstate'),
),
migrations.AlterField(
model_name='ema',
name='before_states',
field=models.ManyToManyField(blank=True, help_text="Refers to 'Ausgangszustand Biotop'", related_name='+', to='compensation.compensationstate'),
),
migrations.AlterField(
model_name='ema',
name='deadlines',
field=models.ManyToManyField(blank=True, related_name='+', to='konova.deadline'),
),
migrations.AlterField(
model_name='ema',
name='resubmissions',
field=models.ManyToManyField(blank=True, related_name='+', to='konova.resubmission'),
),
]

View File

@@ -15,8 +15,11 @@ from django.urls import reverse
from compensation.models import AbstractCompensation, PikMixin from compensation.models import AbstractCompensation, PikMixin
from ema.managers import EmaManager from ema.managers import EmaManager
from ema.settings import EMA_IDENTIFIER_LENGTH, EMA_IDENTIFIER_TEMPLATE, EMA_LANIS_LAYER_NAME_RECORDED, \
EMA_LANIS_LAYER_NAME_UNRECORDED
from ema.utils.quality import EmaQualityChecker from ema.utils.quality import EmaQualityChecker
from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin from konova.models import AbstractDocument, generate_document_file_upload_path, RecordableObjectMixin, ShareableObjectMixin
from konova.sub_settings.django_settings import BASE_URL
from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE from konova.utils.message_templates import DATA_UNSHARED_EXPLANATION, DOCUMENT_REMOVED_TEMPLATE
@@ -38,9 +41,18 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, Pik
""" """
objects = EmaManager() objects = EmaManager()
identifier_length = EMA_IDENTIFIER_LENGTH
identifier_template = EMA_IDENTIFIER_TEMPLATE
def __str__(self): def __str__(self):
return "{}".format(self.identifier) return "{}".format(self.identifier)
def get_detail_url(self):
return reverse("ema: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 # Create new identifier
@@ -105,6 +117,20 @@ class Ema(AbstractCompensation, ShareableObjectMixin, RecordableObjectMixin, Pik
""" """
return reverse("ema:share-token", args=(self.id, self.access_token)) return reverse("ema:share-token", args=(self.id, self.access_token))
def get_lanis_layer_name(self):
""" Getter for specific LANIS/WFS object layer
Returns:
"""
retval = None
if self.is_recorded:
retval = EMA_LANIS_LAYER_NAME_RECORDED
else:
retval = EMA_LANIS_LAYER_NAME_UNRECORDED
return retval
class EmaDocument(AbstractDocument): class EmaDocument(AbstractDocument):
""" """
@@ -122,7 +148,7 @@ class EmaDocument(AbstractDocument):
def delete(self, user=None, *args, **kwargs): def delete(self, user=None, *args, **kwargs):
""" """
Custom delete functionality for EcoAccountDocuments. Custom delete functionality for EmaDocuments.
Removes the folder from the file system if there are no further documents for this entry. Removes the folder from the file system if there are no further documents for this entry.
Args: Args:
@@ -139,8 +165,11 @@ class EmaDocument(AbstractDocument):
# The only file left for this EMA is the one which is currently processed and will be deleted # The only file left for this EMA 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

@@ -6,5 +6,7 @@ Created on: 19.08.21
""" """
EMA_ACCOUNT_IDENTIFIER_LENGTH = 6 EMA_IDENTIFIER_LENGTH = 6
EMA_ACCOUNT_IDENTIFIER_TEMPLATE = "EMA-{}" EMA_IDENTIFIER_TEMPLATE = "EMA-{}"
EMA_LANIS_LAYER_NAME_RECORDED = "ema_recorded"
EMA_LANIS_LAYER_NAME_UNRECORDED = "ema_unrecorded"

View 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>

View File

@@ -25,7 +25,7 @@
{% trans 'Missing finished deadline ' %} {% trans 'Missing finished deadline ' %}
</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>

View 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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -80,6 +80,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
self.ema = self.fill_out_ema(self.ema) self.ema = self.fill_out_ema(self.ema)
pre_edit_log_count = self.ema.log.count() pre_edit_log_count = self.ema.log.count()
old_identifier = self.ema.identifier
new_title = self.create_dummy_string() new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string() new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string() new_comment = self.create_dummy_string()
@@ -106,13 +107,13 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
check_on_elements = { check_on_elements = {
self.ema.title: new_title, self.ema.title: new_title,
self.ema.identifier: new_identifier,
self.ema.comment: new_comment, self.ema.comment: new_comment,
} }
for k, v in check_on_elements.items(): for k, v in check_on_elements.items():
self.assertEqual(k, v) self.assertEqual(k, v)
self.assertEqual(self.ema.identifier, old_identifier)
self.assert_equal_geometries(self.ema.geometry.geom, new_geometry) self.assert_equal_geometries(self.ema.geometry.geom, new_geometry)
# Expect logs to be set # Expect logs to be set

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: 24.08.23
"""

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: 01.09.23
"""
import json
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
from ema.forms import NewEmaForm, EditEmaForm
from konova.forms import SimpleGeomForm
from konova.tests.test_views import BaseTestCase
from konova.utils.generators import generate_random_string
from user.models import UserAction
class NewEmaFormTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_init(self):
form = NewEmaForm()
self.assertEqual(form.form_title, str(_("New EMA")))
self.assertEqual(form.action_url, reverse("ema:new"))
self.assertEqual(form.cancel_redirect, reverse("ema:index"))
self.assertIsNotNone(form.fields["identifier"].initial)
self.assertEqual(form.fields["title"].widget.attrs["placeholder"], str(_("Compensation XY; Location ABC")))
def test_save(self):
cons_office_code = self.get_conservation_office_code()
data = {
"identifier": generate_random_string(length=20, use_numbers=True),
"title": generate_random_string(length=20, use_letters_lc=True),
"conservation_office": cons_office_code,
"conservation_file_number": generate_random_string(length=10, use_numbers=True),
"is_pik": True,
"comment": generate_random_string(length=20, use_numbers=True, use_letters_uc=True),
}
form = NewEmaForm(data)
test_geom = self.create_dummy_geometry()
geom_form_data = self.create_geojson(
test_geom
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)
self.assertTrue(form.is_valid(), msg=form.errors)
self.assertTrue(geom_form.is_valid(), msg=form.errors)
obj = form.save(user=self.superuser, geom_form=geom_form)
self.assertEqual(obj.title, data["title"])
self.assertEqual(obj.is_pik, data["is_pik"])
self.assertIsNotNone(obj.responsible)
self.assertIsNotNone(obj.responsible.handler)
self.assertEqual(obj.responsible.conservation_office, data["conservation_office"])
self.assertEqual(obj.responsible.conservation_file_number, data["conservation_file_number"])
self.assertEqual(obj.identifier, data["identifier"])
self.assertEqual(obj.comment, data["comment"])
self.assertIn(self.superuser, obj.shared_users)
last_log = obj.log.first()
self.assertEqual(obj.created, obj.modified)
self.assertEqual(obj.created, last_log)
self.assertEqual(last_log.action, UserAction.CREATED)
self.assertEqual(last_log.user, self.superuser)
self.assertTrue(test_geom.equals_exact(obj.geometry.geom, 0.000001))
class EditEmaFormTestCase(BaseTestCase):
def test_init(self):
form = EditEmaForm(instance=self.ema)
self.assertEqual(form.form_title, str(_("Edit EMA")))
self.assertEqual(form.action_url, reverse("ema:edit", args=(self.ema.id,)))
self.assertEqual(form.cancel_redirect, reverse("ema:detail", args=(self.ema.id,)))
self.assertEqual(form.fields["identifier"].widget.attrs["url"], reverse("ema:new-id"))
self.assertEqual(form.fields["title"].widget.attrs["placeholder"], str(_("Compensation XY; Location ABC")))
values = {
"identifier": self.ema.identifier,
"title": self.ema.title,
"comment": self.ema.comment,
"conservation_office": self.ema.responsible.conservation_office,
"conservation_file_number": self.ema.responsible.conservation_file_number,
"is_pik": self.ema.is_pik,
"handler_type": self.ema.responsible.handler.type,
"handler_detail": self.ema.responsible.handler.detail,
}
for k, v in values.items():
self.assertEqual(form.fields[k].initial, v)
def test_save(self):
cons_office_code = self.get_conservation_office_code()
data = {
"identifier": generate_random_string(length=20, use_numbers=True),
"title": generate_random_string(length=20, use_letters_lc=True),
"conservation_office": cons_office_code,
"conservation_file_number": generate_random_string(length=10, use_numbers=True),
"is_pik": not self.ema.is_pik,
"comment": generate_random_string(length=20, use_numbers=True, use_letters_uc=True),
}
form = EditEmaForm(data, instance=self.ema)
self.assertTrue(form.is_valid(), msg=form.errors)
test_geom = self.create_dummy_geometry()
geom_form_data = self.create_geojson(
test_geom
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)
self.assertTrue(geom_form.is_valid())
obj = form.save(self.superuser, geom_form)
self.assertEqual(obj.id, self.ema.id)
self.assertEqual(obj.title, data["title"])
self.assertEqual(obj.is_pik, data["is_pik"])
self.assertIsNotNone(obj.responsible)
self.assertIsNotNone(obj.responsible.handler)
self.assertEqual(obj.responsible.conservation_office, data["conservation_office"])
self.assertEqual(obj.responsible.conservation_file_number, data["conservation_file_number"])
self.assertNotEqual(obj.identifier, data["identifier"], msg="Identifier editable via form!")
self.assertEqual(obj.comment, data["comment"])
last_log = obj.log.first()
self.assertEqual(obj.modified, last_log)
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertTrue(test_geom.equals_exact(obj.geometry.geom, 0.000001))

View File

@@ -0,0 +1,90 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 24.08.23
"""
from django.urls import reverse
from django.utils.timezone import now
from ema.models import Ema, EmaDocument
from ema.settings import EMA_IDENTIFIER_TEMPLATE
from konova.tests.test_views import BaseTestCase
from konova.utils.message_templates import DOCUMENT_REMOVED_TEMPLATE
from user.models import UserAction
class EmaModelTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_str(self):
self.assertEqual(str(self.ema), f"{self.ema.identifier}")
def test_save(self):
new_ema = Ema(
title="Test"
)
self.assertIsNone(new_ema.identifier)
new_ema.save()
new_ema.refresh_from_db()
self.assertIsNotNone(new_ema.identifier)
self.assertIn("EMA-", new_ema.identifier)
def test_is_ready_for_publish(self):
self.assertIsNone(self.ema.recorded)
self.assertFalse(self.ema.is_ready_for_publish())
self.ema.set_recorded(self.superuser)
self.ema.refresh_from_db()
self.assertIsNotNone(self.ema.recorded)
self.assertTrue(self.ema.is_ready_for_publish())
def test_get_share_link(self):
self.assertEqual(
self.ema.get_share_link(),
reverse("ema:share-token", args=(self.ema.id, self.ema.access_token))
)
def test_get_documents(self):
self.assertEqual(self.ema.get_documents().count(), 0)
doc = EmaDocument(
instance=self.ema,
date_of_creation=now().date(),
comment="Test",
)
doc.save()
docs = self.ema.get_documents()
self.assertEqual(docs.count(), 1)
self.assertEqual(docs.first(), doc)
class EmaDocumentModelTestCase(BaseTestCase):
def setUp(self) -> None:
super().setUp()
def test_delete(self):
doc = EmaDocument.objects.create(
date_of_creation=now().date(),
instance=self.ema,
comment="TEST"
)
self.ema.refresh_from_db()
docs = self.ema.get_documents()
self.assertEqual(docs.count(), 1)
self.assertEqual(docs.first(), doc)
doc_title = doc.title
doc.delete(user=self.superuser)
last_log = self.ema.log.first()
self.assertEqual(last_log.action, UserAction.EDITED)
self.assertEqual(last_log.user, self.superuser)
self.assertEqual(last_log.comment, DOCUMENT_REMOVED_TEMPLATE.format(doc_title))
docs = self.ema.get_documents()
self.assertEqual(docs.count(), 0)

View File

@@ -17,13 +17,14 @@ from ema.forms import NewEmaForm, EditEmaForm
from ema.models import Ema from ema.models import Ema
from ema.tables import EmaTable from ema.tables import EmaTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
from konova.utils.user_checks import in_group DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
@login_required @login_required
@@ -83,6 +84,12 @@ def new_view(request: HttpRequest):
) )
) )
messages.success(request, _("EMA {} added").format(ema.identifier)) messages.success(request, _("EMA {} added").format(ema.identifier))
if geom_form.geometry_simplified:
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
return redirect("ema:detail", id=ema.id) return redirect("ema:detail", id=ema.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -118,6 +125,7 @@ def new_id_view(request: HttpRequest):
@login_required @login_required
@uuid_required
def detail_view(request: HttpRequest, id: str): def detail_view(request: HttpRequest, id: str):
""" Renders the detail view of an EMA """ Renders the detail view of an EMA
@@ -142,12 +150,19 @@ def detail_view(request: HttpRequest, id: str):
# Precalculate logical errors between before- and after-states # Precalculate logical errors between before- and after-states
# Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling # Sum() returns None in case of no states, so we catch that and replace it with 0 for easier handling
sum_before_states = before_states.aggregate(Sum("surface"))["surface__sum"] or 0 sum_before_states = ema.get_surface_before_states()
sum_after_states = after_states.aggregate(Sum("surface"))["surface__sum"] or 0 sum_after_states = ema.get_surface_after_states()
diff_states = abs(sum_before_states - sum_after_states) diff_states = abs(sum_before_states - sum_after_states)
ema.set_status_messages(request) ema.set_status_messages(request)
requesting_user_is_only_shared_user = ema.is_only_shared_with(_user)
if requesting_user_is_only_shared_user:
messages.info(
request,
DO_NOT_FORGET_TO_SHARE
)
context = { context = {
"obj": ema, "obj": ema,
"geom_form": geom_form, "geom_form": geom_form,
@@ -158,9 +173,9 @@ def detail_view(request: HttpRequest, id: str):
"sum_before_states": sum_before_states, "sum_before_states": sum_before_states,
"sum_after_states": sum_after_states, "sum_after_states": sum_after_states,
"diff_states": diff_states, "diff_states": diff_states,
"is_default_member": in_group(_user, DEFAULT_GROUP), "is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": in_group(_user, ZB_GROUP), "is_zb_member": _user.in_group(ZB_GROUP),
"is_ets_member": in_group(_user, ETS_GROUP), "is_ets_member": _user.in_group(ETS_GROUP),
"LANIS_LINK": ema.get_LANIS_link(), "LANIS_LINK": ema.get_LANIS_link(),
TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}", TAB_TITLE_IDENTIFIER: f"{ema.identifier} - {ema.title}",
"has_finished_deadlines": ema.get_finished_deadlines().exists(), "has_finished_deadlines": ema.get_finished_deadlines().exists(),
@@ -200,6 +215,11 @@ def edit_view(request: HttpRequest, id: str):
# The data form takes the geom form for processing, as well as the performing user # The data form takes the geom form for processing, as well as the performing user
ema = data_form.save(request.user, geom_form) ema = data_form.save(request.user, geom_form)
messages.success(request, _("EMA {} edited").format(ema.identifier)) messages.success(request, _("EMA {} edited").format(ema.identifier))
if geom_form.geometry_simplified:
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
return redirect("ema:detail", id=ema.id) return redirect("ema:detail", id=ema.id)
else: else:
messages.error(request, FORM_INVALID, extra_tags="danger",) messages.error(request, FORM_INVALID, extra_tags="danger",)

View File

@@ -73,6 +73,7 @@ def report_view(request:HttpRequest, id: str):
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels, "parcels": parcels,
"actions": actions, "actions": actions,
"tables_scrollable": False,
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context

View File

@@ -34,3 +34,9 @@ class InterventionAutocomplete(Select2QuerySetView):
Q(title__icontains=self.q) Q(title__icontains=self.q)
).distinct() ).distinct()
return qs return qs
def get_result_label(self, result):
return str(result)
def get_selected_result_label(self, result):
return str(result)

View File

@@ -9,6 +9,7 @@ from dal import autocomplete
from django import forms from django import forms
from konova.forms.base_form import BaseForm from konova.forms.base_form import BaseForm
from konova.utils import validators
from konova.utils.message_templates import EDITED_GENERAL_DATA from konova.utils.message_templates import EDITED_GENERAL_DATA
from user.models import User from user.models import User
from django.db import transaction from django.db import transaction
@@ -29,11 +30,12 @@ class NewInterventionForm(BaseForm):
label=_("Identifier"), label=_("Identifier"),
label_suffix="", label_suffix="",
max_length=255, max_length=255,
help_text=_("Generated automatically"), help_text=_("Generated automatically - not editable"),
widget=GenerateInput( widget=GenerateInput(
attrs={ attrs={
"class": "form-control", "class": "form-control",
"url": reverse_lazy("intervention:new-id"), "url": reverse_lazy("intervention:new-id"),
"readonly": True,
} }
) )
) )
@@ -174,6 +176,7 @@ class NewInterventionForm(BaseForm):
label=_("Registration date"), label=_("Registration date"),
label_suffix=_(""), label_suffix=_(""),
required=False, required=False,
validators=[validators.reasonable_date],
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
"type": "date", "type": "date",
@@ -186,6 +189,7 @@ class NewInterventionForm(BaseForm):
label=_("Binding on"), label=_("Binding on"),
label_suffix=_(""), label_suffix=_(""),
required=False, required=False,
validators=[validators.reasonable_date],
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
"type": "date", "type": "date",
@@ -341,7 +345,6 @@ class EditInterventionForm(NewInterventionForm):
""" """
with transaction.atomic(): with transaction.atomic():
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
process_type = self.cleaned_data.get("type", None) process_type = self.cleaned_data.get("type", None)
laws = self.cleaned_data.get("laws", None) laws = self.cleaned_data.get("laws", None)
@@ -375,7 +378,6 @@ class EditInterventionForm(NewInterventionForm):
self.instance.log.add(user_action) self.instance.log.add(user_action)
self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.comment = comment self.instance.comment = comment
self.instance.modified = user_action self.instance.modified = user_action
@@ -385,6 +387,7 @@ class EditInterventionForm(NewInterventionForm):
geometry = geom_form.save(user_action) geometry = geom_form.save(user_action)
self.instance.geometry = geometry self.instance.geometry = geometry
self.instance.save() self.instance.save()
self.instance.send_data_to_egon()
return self.instance return self.instance

View File

@@ -33,7 +33,7 @@ class CheckModalForm(BaseModalForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Run check") self.form_title = _("Run check")
self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) self.form_caption = _("The necessary control steps have been performed:").format(self.user.first_name, self.user.last_name)
self.valid = False self.valid = False
def _are_deductions_valid(self): def _are_deductions_valid(self):
@@ -43,6 +43,7 @@ class CheckModalForm(BaseModalForm):
""" """
deductions = self.instance.deductions.all() deductions = self.instance.deductions.all()
valid = True
for deduction in deductions: for deduction in deductions:
checker = deduction.account.quality_check() checker = deduction.account.quality_check()
for msg in checker.messages: for msg in checker.messages:
@@ -50,8 +51,8 @@ class CheckModalForm(BaseModalForm):
"checked_comps", "checked_comps",
f"{deduction.account.identifier}: {msg}" f"{deduction.account.identifier}: {msg}"
) )
return checker.valid valid &= checker.valid
return True return valid
def _are_comps_valid(self): def _are_comps_valid(self):
""" Performs validity checks on all types of compensations """ Performs validity checks on all types of compensations
@@ -74,7 +75,8 @@ class CheckModalForm(BaseModalForm):
"checked_comps", "checked_comps",
f"{comp.identifier}: {msg}" f"{comp.identifier}: {msg}"
) )
comps_valid = checker.valid if comps_valid and not checker.valid:
comps_valid = checker.valid
deductions_valid = self._are_deductions_valid() deductions_valid = self._are_deductions_valid()
return deductions_valid and comps_valid return deductions_valid and comps_valid

View File

@@ -25,8 +25,6 @@ class NewInterventionDocumentModalForm(NewDocumentModalForm):
""" """
doc = super().save(*args, **kwargs) doc = super().save(*args, **kwargs)
self.instance.send_data_to_egon()
if self.instance.payments.exists():
self.instance.send_data_to_egon()
return doc return doc

View File

@@ -11,6 +11,7 @@ from django.utils.translation import gettext_lazy as _
from intervention.models import RevocationDocument from intervention.models import RevocationDocument
from konova.forms.modals import BaseModalForm, RemoveModalForm from konova.forms.modals import BaseModalForm, RemoveModalForm
from konova.utils import validators
from konova.utils.message_templates import REVOCATION_ADDED, REVOCATION_EDITED from konova.utils.message_templates import REVOCATION_ADDED, REVOCATION_EDITED
@@ -19,6 +20,7 @@ class NewRevocationModalForm(BaseModalForm):
label=_("Date"), label=_("Date"),
label_suffix=_(""), label_suffix=_(""),
help_text=_("Date of revocation"), help_text=_("Date of revocation"),
validators=[validators.reasonable_date],
widget=forms.DateInput( widget=forms.DateInput(
attrs={ attrs={
"type": "date", "type": "date",

View File

@@ -12,7 +12,6 @@ from django.utils.translation import gettext_lazy as _
from intervention.inputs import TextToClipboardInput from intervention.inputs import TextToClipboardInput
from konova.forms.modals import BaseModalForm from konova.forms.modals import BaseModalForm
from konova.utils.message_templates import ENTRY_REMOVE_MISSING_PERMISSION from konova.utils.message_templates import ENTRY_REMOVE_MISSING_PERMISSION
from konova.utils.user_checks import is_default_group_only
from user.models import Team, User from user.models import Team, User
@@ -80,7 +79,7 @@ class ShareModalForm(BaseModalForm):
teams = self.cleaned_data.get("teams", Team.objects.none()) teams = self.cleaned_data.get("teams", Team.objects.none())
_is_valid = True _is_valid = True
if is_default_group_only(self.user): if self.user.is_default_group_only():
shared_users = self.instance.shared_users shared_users = self.instance.shared_users
shared_teams = self.instance.shared_teams shared_teams = self.instance.shared_teams

View File

@@ -0,0 +1,19 @@
# Generated by Django 4.2.6 on 2023-11-30 11:56
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0014_resubmission'),
('intervention', '0008_auto_20221116_1322'),
]
operations = [
migrations.AlterField(
model_name='intervention',
name='resubmissions',
field=models.ManyToManyField(blank=True, related_name='+', to='konova.resubmission'),
),
]

View File

@@ -13,7 +13,12 @@ from django.db.models.fields.files import FieldFile
from django.urls import reverse from django.urls import reverse
from django.utils import timezone from django.utils import timezone
from analysis.settings import LKOMPVZVO_PUBLISH_DATE
from intervention.settings import INTERVENTION_IDENTIFIER_LENGTH, INTERVENTION_IDENTIFIER_TEMPLATE, \
INTERVENTION_LANIS_LAYER_NAME_RECORDED, INTERVENTION_LANIS_LAYER_NAME_UNRECORDED_OLD_ENTRY, \
INTERVENTION_LANIS_LAYER_NAME_UNRECORDED
from intervention.tasks import celery_export_to_egon from intervention.tasks import celery_export_to_egon
from konova.sub_settings.django_settings import BASE_URL
from user.models import User from user.models import User
from django.db import models, transaction from django.db import models, transaction
from django.db.models import QuerySet from django.db.models import QuerySet
@@ -59,9 +64,18 @@ class Intervention(BaseObject,
objects = InterventionManager() objects = InterventionManager()
identifier_length = INTERVENTION_IDENTIFIER_LENGTH
identifier_template = INTERVENTION_IDENTIFIER_TEMPLATE
def __str__(self): def __str__(self):
return f"{self.identifier} ({self.title})" return f"{self.identifier} ({self.title})"
def get_detail_url(self):
return reverse("intervention: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):
""" Custom save functionality """ Custom save functionality
@@ -145,11 +159,13 @@ class Intervention(BaseObject,
Returns: Returns:
""" """
celery_export_to_egon.delay(self.id) if self.payments.exists():
celery_export_to_egon.delay(self.id)
def set_recorded(self, user: User) -> UserActionLogEntry: def set_recorded(self, user: User) -> UserActionLogEntry:
log_entry = super().set_recorded(user) log_entry = super().set_recorded(user)
self.add_log_entry_to_compensations(log_entry) self.add_log_entry_to_compensations(log_entry)
self.send_data_to_egon()
return log_entry return log_entry
def add_log_entry_to_compensations(self, log_entry: UserActionLogEntry): def add_log_entry_to_compensations(self, log_entry: UserActionLogEntry):
@@ -272,26 +288,45 @@ class Intervention(BaseObject,
revocation.delete() revocation.delete()
self.mark_as_edited(user, request=form.request, edit_comment=REVOCATION_REMOVED) self.mark_as_edited(user, request=form.request, edit_comment=REVOCATION_REMOVED)
def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None, reset_recorded: bool = True): def mark_as_edited(self, performing_user: User, request: HttpRequest = None, edit_comment: str = None):
""" In case the object or a related object changed, internal processes need to be started, such as """ Log the edit action
unrecord and uncheck
If the object is checked, set it to unchecked due to the editing. Another check is needed then.
Args: Args:
performing_user (User): The user which performed the editing action performing_user (User): The user which performed the editing action
request (HttpRequest): The used request for this action request (HttpRequest): The used request for this action
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:
""" """
action = super().mark_as_edited(performing_user, edit_comment=edit_comment) action = super().mark_as_edited(performing_user, edit_comment=edit_comment)
if reset_recorded:
self.unrecord(performing_user, request)
if self.checked: if self.checked:
self.set_unchecked() self.set_unchecked()
return action return action
def mark_as_deleted(self, user, send_mail: bool = True):
""" Extends base mark_as_delete functionality
Removes related deductions from the database, which results in updating the deductable_rest of the
corresponding eco account.
Args:
user (User): The performing user
send_mail (bool): Whether to send an info mail
Returns:
"""
super().mark_as_deleted(user, send_mail)
# Remove pending deductions to free booked capacities
deductions = self.deductions.all()
# Remove one by one instead of bulk to trigger EcoAccountDeduction custom delete() logic
for deduction in deductions:
deduction.delete()
def set_status_messages(self, request: HttpRequest): def set_status_messages(self, request: HttpRequest):
""" Setter for different information that need to be rendered """ Setter for different information that need to be rendered
@@ -322,14 +357,16 @@ class Intervention(BaseObject,
is_ready (bool) : True|False is_ready (bool) : True|False
""" """
now_date = timezone.now().date() now_date = timezone.now().date()
binding_date = self.legal.binding_date # use current date as fallback if binding_date does not exist --> is_old_entry will fail as we want it for this case
binding_date = self.legal.binding_date or timezone.now().date()
is_old_entry = binding_date < LKOMPVZVO_PUBLISH_DATE
is_binding_date_ready = binding_date is not None and binding_date <= now_date is_binding_date_ready = binding_date is not None and binding_date <= now_date
is_recorded = self.recorded is not None is_recorded = self.recorded is not None
is_free_of_revocations = not self.legal.revocations.exists() is_free_of_revocations = not self.legal.revocations.exists()
is_ready = is_binding_date_ready \ is_ready = is_binding_date_ready \
and is_recorded \ and is_recorded \
and is_free_of_revocations and is_free_of_revocations
return is_ready return is_ready or is_old_entry
def get_share_link(self): def get_share_link(self):
""" Returns the share url for the object """ Returns the share url for the object
@@ -355,6 +392,28 @@ class Intervention(BaseObject,
self.mark_as_edited(user, request=form.request, edit_comment=PAYMENT_REMOVED) self.mark_as_edited(user, request=form.request, edit_comment=PAYMENT_REMOVED)
self.send_data_to_egon() self.send_data_to_egon()
def get_lanis_layer_name(self):
""" Getter for specific LANIS/WFS object layer
Returns:
"""
retval = None
if self.is_recorded:
retval = INTERVENTION_LANIS_LAYER_NAME_RECORDED
else:
try:
is_old_entry = self.legal.binding_date < LKOMPVZVO_PUBLISH_DATE
except TypeError:
is_old_entry = False
if is_old_entry:
retval = INTERVENTION_LANIS_LAYER_NAME_UNRECORDED_OLD_ENTRY
else:
retval = INTERVENTION_LANIS_LAYER_NAME_UNRECORDED
return retval
class InterventionDocument(AbstractDocument): class InterventionDocument(AbstractDocument):
""" """
@@ -389,8 +448,11 @@ class InterventionDocument(AbstractDocument):
# The only file left for this intervention is the one which is currently processed and will be deleted # The only file left for this intervention is the one which is currently processed and will be deleted
# Make sure that the intervention folder itself is deleted as well, not only the file # Make sure that the intervention 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

@@ -79,6 +79,10 @@ class RevocationDocument(AbstractDocument):
# Remove the file itself # Remove the file itself
super().delete(*args, **kwargs) super().delete(*args, **kwargs)
if not self.file:
# If (for reasons) no file path has been added to the entry, we act as if the file did not exist
raise ObjectDoesNotExist
# Always remove 'revocation' folder if the one revocation we just processed is the only one left # Always remove 'revocation' folder if the one revocation we just processed is the only one left
folder_path = self.file.path.split("/") folder_path = self.file.path.split("/")
if revoc_docs.count() == 0: if revoc_docs.count() == 0:

View File

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

View File

@@ -33,6 +33,11 @@ class InterventionTable(BaseTable, TableRenderMixin, TableOrderMixin):
verbose_name=_("Parcel gmrkng"), verbose_name=_("Parcel gmrkng"),
orderable=False, orderable=False,
accessor="geometry", accessor="geometry",
attrs={
"th": {
"class": "w-25",
}
}
) )
c = tables.Column( c = tables.Column(
verbose_name=_("Checked"), verbose_name=_("Checked"),

View File

@@ -22,7 +22,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>

View 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>

View File

@@ -25,7 +25,7 @@
{% trans 'You entered a payment. Please upload the legal document which defines the payment`s amount.' %} {% trans 'You entered a payment. Please upload the legal document which defines the payment`s amount.' %}
</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>

View 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>

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