Compare commits

...

218 Commits

Author SHA1 Message Date
31de477f26 Merge pull request 'master' (#516) from master into Docker
Reviewed-on: #516
2025-12-19 14:18:12 +01:00
ee2c859a9e Merge pull request '# Improve exception reporting for API' (#515) from improve_exception_reporting into master
Reviewed-on: #515
2025-12-19 14:17:37 +01:00
328f672ec0 # Improve exception reporting for API
* fixes typo in exception_reporter.py
* properly catches error on geometry cast into multipolygon if input are no valid polygons
* extends error response on malicious api calls
* specifies different exceptions on try-catch while initializing api data
2025-12-19 14:17:15 +01:00
19b6e633df Merge pull request 'master' (#514) from master into Docker
Reviewed-on: #514
2025-12-17 14:04:03 +01:00
047c9489fe Merge pull request '# ExceptionReporter adjustment' (#513) from improve_exception_reporting into master
Reviewed-on: #513
2025-12-17 14:03:23 +01:00
38b81996ed # ExceptionReporter adjustment
* extends the KonovaExceptionReporter to hold POST body content (practical for debugging broken content on API)
2025-12-17 14:02:08 +01:00
b98f821c98 Merge pull request 'master' (#511) from master into Docker
Reviewed-on: #511
2025-12-03 13:50:13 +01:00
4c4d64cc3d Merge pull request '# HOTFIX: empty geometry save' (#510) from hotfix_empty_geometry_save into master
Reviewed-on: #510
2025-12-03 13:49:55 +01:00
fbde03caec # Optimization
* optimizes logic in case of empty geometry by dropping redundant pre-check on emptiness
2025-12-03 13:48:58 +01:00
43eb598d3f # HOTFIX: empty geometry save
* fixes a bug where the saving of an empty geometry could lead into a json decode error
2025-12-03 13:38:13 +01:00
5421de4e80 Merge pull request 'master' (#509) from master into Docker
Reviewed-on: #509
2025-11-30 12:33:58 +01:00
b7fac0ae03 Merge pull request '# Fix fpr #507' (#508) from 507_Improper_deduction-recording_rendering_on_unrecorded_eco_account into master
Reviewed-on: #508
2025-11-30 12:33:39 +01:00
447ba942b5 # Fix fpr #507
* fixes incorrect rendering of recording-info for deductions on unrecorded eco accounts
2025-11-30 12:32:05 +01:00
a7a0044fc5 Merge pull request 'master' (#506) from master into Docker
Reviewed-on: #506
2025-11-28 11:45:48 +01:00
6df47f1615 Merge pull request '504_Geometry_read-only_on_editing' (#505) from 504_Geometry_read-only_on_editing into master
Reviewed-on: #505
2025-11-28 11:45:30 +01:00
e25d549a97 # 497 Impressum link update
* updates impressum link
2025-11-28 11:44:18 +01:00
5e65b8f4dc # Geometry error message fix
* fixes bug where errors on geometry form were not rendered properly
* fixes bug where invalid geometry was written as read-only back into form (could not be corrected by user)
* adds explanatory comments to SimpleGeomForm is_valid() checks
* reorders code snippets for better understanding
* adds correcting logic to _set_geojson_properties() in case of missing properties element
2025-11-28 11:43:17 +01:00
36552b3886 Merge pull request 'master' (#502) from master into Docker
Reviewed-on: #502
2025-11-19 13:16:48 +01:00
22cddb9902 Merge pull request '# Fix for #500' (#501) from 500_Geometry_conflicts_still_visible into master
Reviewed-on: #501
2025-11-19 13:16:30 +01:00
c986bd0b92 # Fix for #500
* fixes bug where de-facto deleted compensations (because of deleted intervention) would still show up as geometry conflicts on other entries
2025-11-19 13:16:09 +01:00
a766c4dbe8 Merge pull request 'master' (#499) from master into Docker
Reviewed-on: #499
2025-11-07 14:12:18 +01:00
2c60d86177 Merge pull request '# Update netgis map client' (#498) from netgis_map_client into master
Reviewed-on: #498
2025-11-07 14:11:57 +01:00
b7792ececc # Update netgis map client
* updates netgis map client (bugfix for https://github.com/sebastianpauli/netgis-client/issues/43#issuecomment-3446898016)
2025-11-07 14:11:15 +01:00
8126781b77 Merge pull request 'master' (#496) from master into Docker
Reviewed-on: #496
2025-10-23 16:13:04 +02:00
210f3fcafa Merge pull request '# HOTFIX' (#495) from update_map_config into master
Reviewed-on: #495
2025-10-23 16:12:45 +02:00
e7d67560f2 # HOTFIX
* fixes bug on netgis map client where importing a geometry would result in an error message
* THIS IS JUST A WORKAROUND AND HAS TO BE REPLACED BY A PROPER FIX FROM THE DEVS ASAP
2025-10-23 16:12:05 +02:00
b5e991fb95 Merge pull request '# Map layer update' (#494) from update_map_config into master
Reviewed-on: #494
2025-10-22 16:02:02 +02:00
d3a555d406 # Map layer update
* updates rp_dop layer from deprecated to newest
2025-10-22 16:00:35 +02:00
a6a66d7499 Merge pull request 'master' (#493) from master into Docker
Reviewed-on: #493
2025-10-21 15:59:53 +02:00
9a374f50de Merge pull request '# CSS bugfix' (#492) from fix_css_map_menu_bar_overlap into master
Reviewed-on: #492
2025-10-21 15:59:37 +02:00
ce63dd30bc # CSS bugfix
* fixes a rendering bug where the menu bar of the map client overlapped modal forms
2025-10-21 15:57:25 +02:00
1c0b67693d Merge pull request 'master' (#489) from master into Docker
Reviewed-on: #489
2025-10-15 09:51:46 +02:00
e5db7f6b13 Merge pull request '# Small geometry processing' (#488) from 487_Small_geometry_processing into master
Reviewed-on: #488
2025-10-15 09:51:27 +02:00
442f3ceb37 # Small geometry processing
* changes SimpleGeomForm behaviour on small geometries (<1m²): These geometries will now be dismissed on processing
* adds a new info message in case of automatically removed geometries on saving
* updates tests
2025-10-15 09:50:59 +02:00
ce6bb6b23b Merge pull request 'master' (#486) from master into Docker
Reviewed-on: #486
2025-10-12 11:32:27 +02:00
cd2949fc03 Merge pull request 'netgis_map_client_update' (#452) from netgis_map_client_update into master
Reviewed-on: #452
2025-10-12 11:31:37 +02:00
7bcd32fd7a # Netgis client update
* minor changes to configuration
2025-10-12 11:30:04 +02:00
97f1882698 Merge branch 'refs/heads/master' into netgis_map_client_update
# Conflicts:
#	konova/forms/geometry_form.py
#	templates/map/client/config.json
2025-10-12 11:22:56 +02:00
55cc8eb1f3 # Netgis client update
* updates the map client plugin to the latest and stable release
2025-10-12 11:18:11 +02:00
0b8176db2e Merge pull request 'master' (#484) from master into Docker
Reviewed-on: #484
2025-09-22 12:37:34 +02:00
d6cabd5f6c Merge pull request 'sso_propagation_extension' (#483) from sso_propagation_extension into master
Reviewed-on: #483
2025-09-22 12:37:16 +02:00
63a824f9d9 # Geometry form fix
* fixes bugs in tests
* refactors and simplifies geometry merging on GeometryForm
2025-09-12 13:22:35 +02:00
a12c2fb57e # Propagation extension
* adds sso_identifier as new User model attribute
* refactors minor code snippets on user-propagation data resolving
* adds updating of username based on propagation data
* adds sso_identifier on admin backend view
2025-09-12 09:24:02 +02:00
3a299a040a Merge pull request 'master' (#482) from master into Docker
Reviewed-on: #482
2025-08-18 08:47:01 +02:00
23bc79ee3b Merge pull request '# Hotfix #480' (#481) from 480_API_error into master
Reviewed-on: #481
2025-08-18 08:46:47 +02:00
07bac26a58 # Hotfix #480
* (potentially) fixes a bug occuring on non multipolygon geometries processed in an api call
* simplifies casting into multipolygon
* simplifies casting into rlp srid (epsg:25832)
2025-08-18 08:46:21 +02:00
3c5206139b Merge pull request 'master' (#477) from master into Docker
Reviewed-on: #477
2025-05-12 15:40:21 +02:00
d01816cf71 Merge pull request '475_Wrong_uuid' (#476) from 475_Wrong_uuid into master
Reviewed-on: #476
2025-05-12 15:39:59 +02:00
f543dfc1cb # Issue 464
* updates DOP map layer to most recent DOP20
2025-05-12 15:26:47 +02:00
62fc019127 # Issue 475
* adds proper handling in case of BadRequest (error 400)
* enhances html template for error 500
* adds new html template for error 400
* adds uuid_required decorator to missing views
* updates translations
2025-05-12 15:22:43 +02:00
c01518b675 # CSS
* changes netgis map client menu background to matching color scheme for konova
2025-05-09 16:59:34 +02:00
f57306eb72 # New version
* adds the newest version of the netgis client
* updates config json to match new config syntax and make use of new features
2025-05-09 16:46:14 +02:00
c2b12649b0 Merge branch 'refs/heads/master' into netgis_map_client_update
# Conflicts:
#	templates/map/client/config.json
2025-05-09 16:03:35 +02:00
6c53f39a28 Merge pull request 'master' (#474) from master into Docker
Reviewed-on: #474
2025-03-28 16:41:31 +01:00
6fe67a8fbf Merge pull request '# HOTFIX' (#473) from hotfix into master
Reviewed-on: #473
2025-03-28 16:41:08 +01:00
b38a266bea # HOTFIX
* fixes bug where new wsg84 check on intersection microservice did not allow other spatial systems anymore
2025-03-28 16:40:50 +01:00
64d8f47174 Merge pull request 'Docker_enhanced' (#472) from Docker_enhanced into Docker
Reviewed-on: #472
2025-03-28 16:11:18 +01:00
f5f3246e89 # Docker enhancements
* optimizes nginx.conf
   * better logging of proxied requests
2025-03-24 14:17:08 +01:00
ad8961ab82 # Docker enhancements
* optimizes nginx.conf
   * better proxy pipelining
* optimizes Dockerfile
   * smaller resulting image
   * faster rebuilding due to reusing of existing layers
* optimizes docker-entrypoint.sh
   * better startup performance
   * better compatibility with docker engine
2025-03-24 13:52:31 +01:00
c2c8630c82 Merge pull request 'master' (#471) from master into Docker
Reviewed-on: #471
2025-02-14 15:33:19 +01:00
15f86e866b Merge pull request '# Map config' (#470) from map_config into master
Reviewed-on: #470
2025-02-14 15:32:47 +01:00
c85b136f0a # Map config
* adds latest map config to repository
2025-02-14 15:31:34 +01:00
dce9e1fc71 # Enhancements
* increases nginx max POST body size to 25MB (document upload)
* limits package requests on version 2.32 due to dependency of kombu to this version
2025-01-24 16:21:55 +01:00
2b84bab1d0 Merge pull request 'master' (#469) from master into Docker
Reviewed-on: #469
2025-01-24 16:12:33 +01:00
faf8aed777 Merge pull request '# Drop django-simple-sso' (#468) from 467_Remove_django-simple-sso into master
Reviewed-on: #468
2025-01-24 16:12:06 +01:00
94c498866f # Drop django-simple-sso
* drops django-simple-sso package from project
* drops unused messenger.py
2025-01-24 16:11:23 +01:00
303583daa1 Merge pull request 'master' (#466) from master into Docker
Reviewed-on: #466
2025-01-21 13:44:14 +01:00
616965c890 Merge pull request 'bugfix' (#465) from bugfix into master
Reviewed-on: #465
2025-01-21 13:43:46 +01:00
e39c7eb51f # KSP Token optimization
* adds support for standardized bearer token usage instead of ksptoken/kspuser header usage (still supported)
2025-01-21 13:38:37 +01:00
19bd408fbd # Bugfix code update
* fixes bug where empty short names were not resolved properly
2025-01-21 12:52:46 +01:00
d07b2ffbfb Merge pull request 'master' (#463) from master into Docker
Reviewed-on: #463
2025-01-08 16:05:15 +01:00
7bfe6a37f8 Merge pull request '# 456 Rework API key creation' (#462) from 456_Rework_API_key_creation into master
Reviewed-on: #462
2025-01-08 16:04:12 +01:00
9b63307f01 # 456 Rework API key creation
* removes frontend input field holding generated API key
* replaces with modal form
* reworks tests on API token form
2025-01-08 16:03:26 +01:00
123a470006 Merge pull request '# 457 User autocomplete fix' (#461) from 457_User_autocomplete_fix into master
Reviewed-on: #461
2025-01-08 14:36:01 +01:00
5a0c5285e7 # 457 User autocomplete fix
* fixes bug where empty query parameter would show users in autocomplete share view
* fixes same behaviour on autocomplete share team view
2025-01-08 14:35:35 +01:00
3a01eaff92 Merge pull request '# 450 Optimization recalculate_parcels command' (#460) from 450_Optimierung_Kommando_Nachverschneidung into master
Reviewed-on: #460
2025-01-08 14:27:55 +01:00
2af91aa178 # 450 Optimization recalculate_parcels command
* optimizes recalculate_parcels.py command so that only non-empty geometries will be processed
* drops test_identifier_generating.py command due to missing usage
2025-01-08 14:27:23 +01:00
335800c44b Merge pull request 'master' (#459) from master into Docker
Reviewed-on: #459
2024-12-23 13:42:36 +01:00
53d0af89ac Merge pull request '# Hotfix' (#458) from oauth_fix into master
Reviewed-on: #458
2024-12-23 13:41:53 +01:00
7b5c1f0d97 # Hotfix
* fixes bug where anonymous user trying to logout would throw error
2024-12-23 13:41:25 +01:00
5766cfde47 Merge pull request 'master' (#454) from master into Docker
Reviewed-on: #454
2024-12-23 12:09:47 +01:00
ef076c0b3b Merge pull request 'oauth_fix' (#453) from oauth_fix into master
Reviewed-on: #453
2024-12-23 12:09:20 +01:00
72a5075f3b # Update dependencies
* updates requirements.txt
2024-12-23 12:03:15 +01:00
d677ac6b5a # Map proxy enhancement
* adds whitelisting for map proxy hosts
2024-12-23 11:08:41 +01:00
9149e4cbd3 # Propagation improvement
* fixes documentation and variable names on oauth token revocation
* introduces private key for propagation
* changes key usage in decryption of propagated user data from oauth_client_id to private propagation key
2024-12-23 10:45:08 +01:00
1c24cbea26 # OAuth Token revocation
* adds revocation of user tokens on logout
2024-12-23 09:26:14 +01:00
mipel
bc9cc09372 # Config update
* updates config for newest netgis map client
2024-12-19 12:20:14 +01:00
24518465f3 # Improvement
* adds catch for undecodeable proxied content
2024-11-21 15:08:38 +01:00
457548da4d # Netgis client update
* integrates newest netgis map client
* generalizes map proxy response handling
2024-11-21 13:46:35 +01:00
2ed3fcc0f9 Merge pull request 'master' (#449) from master into Docker
Reviewed-on: #449
2024-11-13 16:09:48 +01:00
fa89bbba99 Merge pull request '# Bugfix: Recalculate_parcels command' (#448) from bugfixing into master
Reviewed-on: #448
2024-11-13 16:09:22 +01:00
78eb711057 # Bugfix: Recalculate_parcels command
* fixes a bug on recalculate_parcels if not --force-all is used
2024-11-13 16:08:36 +01:00
6ff67d12c9 # WIP: Integration netgis client
* adds adjustments for integration of newest netgis client version (81fa3bef48)
2024-11-02 13:06:32 +01:00
3c1cbcd0bd # Netgis map update
* implements newest version of netgis map client
2024-10-30 14:09:10 +01:00
bf72295615 Merge pull request 'master' (#447) from master into Docker
Reviewed-on: #447
2024-10-26 10:25:06 +02:00
416ad8478c Merge pull request '439_Wartungskommando_Nachverschneidung' (#446) from 439_Wartungskommando_Nachverschneidung into master
Reviewed-on: #446
2024-10-26 10:24:50 +02:00
6b28c4ec15 # Drop atomic transaction
* drops atomic transaction processing on Parcel.make_unique
2024-10-26 10:24:10 +02:00
46a2a4ff46 # Parcel recalculation optimization
* enhances workflow for parcel recalculation
2024-10-26 10:17:09 +02:00
6b860f8ea5 Merge pull request 'master' (#445) from master into Docker
Reviewed-on: #445
2024-10-26 09:48:50 +02:00
90e5cf5b36 Merge pull request '# Parcel duplicate repair' (#444) from 439_Wartungskommando_Nachverschneidung into master
Reviewed-on: #444
2024-10-26 09:48:32 +02:00
50f46e319c # Parcel duplicate repair
* adds mechanic to repair parcels in case of unwanted parcel duplicates
* optimizes filtering of geometries for parcel recalculation
2024-10-26 09:47:27 +02:00
2fa2fa547b Merge pull request 'master' (#443) from master into Docker
Reviewed-on: #443
2024-10-25 19:27:23 +02:00
e2ea087c4e Merge pull request '# Wartungskommando Optimization' (#442) from 439_Wartungskommando_Nachverschneidung into master
Reviewed-on: #442
2024-10-25 19:27:02 +02:00
a6e43b044b # Wartungskommando Optimization
* extends filtering for recalculatable geometries to records without started calculation at any point (parcel_update_start is null)
* catches exceptions on geometries which could not be recalculated properly, adds them to output for further analysis
* simplifies complexity factor calculation
2024-10-25 19:19:08 +02:00
3de956872c Merge pull request 'master' (#441) from master into Docker
Reviewed-on: #441
2024-10-25 14:24:55 +02:00
be0d261e81 Merge pull request '# 439 Wartungskommando Nachverschneidung' (#440) from 439_Wartungskommando_Nachverschneidung into master
Reviewed-on: #440
2024-10-25 14:24:18 +02:00
62e1b046c3 # 439 Wartungskommando Nachverschneidung
* refactors command update_all_parcels into recalculate_parcels
* fixes bug in command generate_report
2024-10-25 14:23:21 +02:00
1c8e3992d6 Merge pull request 'master' (#438) from master into Docker
Reviewed-on: #438
2024-08-26 18:57:35 +02:00
669a12410f Merge pull request 'missing_migrations' (#437) from missing_migrations into master
Reviewed-on: #437
2024-08-26 18:57:10 +02:00
dd77e6c16e Merge branch 'refs/heads/master' into missing_migrations 2024-08-26 18:53:11 +02:00
33774ce557 # Migrations
* adds missing migrations
* renames variables shadowing in-builts
2024-08-26 18:51:58 +02:00
e6e9e141c8 Merge pull request 'master' (#436) from master into Docker
Reviewed-on: #436
2024-08-19 18:35:17 +02:00
dc3dc99b3d Merge pull request '# User filtering' (#435) from 433_Filter_by_user into master
Reviewed-on: #435
2024-08-19 11:42:18 +02:00
315f9de958 # User filtering
* adds query filter to search for logged users on entries
2024-08-19 11:38:09 +02:00
0726c15086 Merge pull request '432_Unreadable_payments' (#434) from 432_Unreadable_payments into master
Reviewed-on: #434
2024-08-19 10:26:40 +02:00
2492a8abe8 # Codelist migration optimization
* adds boolean to de-/activate migration logic inside of 0002_migrate_975_to_288.py
2024-08-19 10:23:05 +02:00
dbc5cba5d7 # Variable refactoring
* renames variable `has_access` into `is_entry_shared` for better understanding in various places (mostly html related)
2024-08-19 09:44:45 +02:00
c8948ddaea # Censor payments
* censor payments if entry is not shared with user
* updates translations
2024-08-19 09:39:58 +02:00
f8ece06ee8 Merge pull request 'master' (#431) from master into Docker
Reviewed-on: #431
2024-08-07 12:07:22 +02:00
5039da28aa Merge pull request '# Hotfix' (#430) from 427_Integration_of_codelist_288 into master
Reviewed-on: #430
2024-08-07 12:07:04 +02:00
4567339570 # Hotfix
* fixes requirements dependency
2024-08-07 12:06:27 +02:00
149a351bfd Merge pull request 'master' (#429) from master into Docker
Reviewed-on: #429
2024-08-07 12:02:21 +02:00
768849e646 Merge pull request '427_Integration_of_codelist_288' (#428) from 427_Integration_of_codelist_288 into master
Reviewed-on: #428
2024-08-07 12:01:46 +02:00
ebf10645fc # Requirements update
* updates some packages in requirements.txt
2024-08-07 09:17:20 +02:00
df241747cf # Migration list 975->288
* adds migration for app codelist to migrate existing biotope type details codes from list 975 to 288 depending on their atomID
* improves rendering of action and biotope type details on frontend for KOM, OEK and EMA
* refactors KonovaCode str() rendering
2024-08-07 09:12:38 +02:00
94e9035e10 # Codelist 288
* introduces 288 to codelist/settings.py
* refactors usage from 975 to 288
* enhances rendering of codelist names depending on which name exists (short vs long)
2024-08-06 15:39:01 +02:00
0164717b8e Merge pull request 'master' (#426) from master into Docker
Reviewed-on: #426
2024-08-06 14:28:41 +02:00
104952bfc3 Merge pull request 'master' (#423) from master into Docker
Reviewed-on: #423
2024-07-10 09:30:30 +02:00
f96241c8d1 Merge pull request 'master' (#421) from master into Docker
Reviewed-on: #421
2024-07-10 09:27:09 +02:00
ac6b534f58 Merge pull request 'master' (#418) from master into Docker
Reviewed-on: #418
2024-07-08 18:44:22 +02:00
06910cd69a # Image tag
* increases image tag
2024-07-05 10:56:10 +02:00
a48ba520fc Merge branch 'refs/heads/master' into Docker
# Conflicts:
#	konova/celery.py
2024-07-05 10:52:13 +02:00
9f18aa5890 Merge pull request 'master' (#414) from master into Docker
Reviewed-on: #414
2024-07-04 11:42:20 +02:00
ab3bd84f3b # Docker enhancement
* adds logging for gunicorn by default
* adds image tagging
* drops docker-compose environment setting in favor of .env usage (needs to be copied from .env.sample)
2024-07-04 09:37:13 +02:00
f829cd5a4c Merge branch 'refs/heads/master' into Docker
# Conflicts:
#	konova/sub_settings/django_settings.py
#	konova/sub_settings/sso_settings.py
#	requirements.txt
2024-07-04 09:30:41 +02:00
0f2bf95b71 Merge pull request 'master' (#409) from master into Docker
Reviewed-on: #409
2024-06-18 11:50:43 +02:00
6a307016ec Merge pull request 'master' (#403) from master into Docker
Reviewed-on: #403
2024-05-17 10:59:17 +02:00
51017ef8fa Merge pull request 'master' (#401) from master into Docker
Reviewed-on: #401
2024-05-17 07:54:16 +02:00
05560534bc Merge pull request 'master' (#399) from master into Docker
Reviewed-on: #399
2024-05-16 17:37:56 +02:00
c882173e78 Merge branch 'refs/heads/master' into Docker
# Conflicts:
#	konova/sub_settings/sso_settings.py
#	requirements.txt
2024-05-16 15:22:57 +02:00
1d94211428 # Requirements update
* fixes requirements.txt due to django-simple-sso dependency
2024-04-12 08:51:18 +02:00
37357080d8 # Gunicorn update
* updates gunicorn package
2024-04-12 08:08:27 +02:00
5afa13ac92 Merge branch 'refs/heads/master' into Docker
# Conflicts:
#	requirements.txt
2024-04-12 08:07:05 +02:00
416cad1c8f Merge pull request 'master' (#392) from master into Docker
Reviewed-on: SGD-Nord/konova#392
2024-04-02 08:11:08 +02:00
b5f83b7163 Merge pull request '# Requirements' (#390) from master into Docker
Reviewed-on: SGD-Nord/konova#390
2024-03-11 08:22:35 +01:00
20cfb5f345 Merge pull request 'master' (#389) from master into Docker
Reviewed-on: SGD-Nord/konova#389
2024-02-29 18:39:41 +01:00
88c96b95f2 Merge pull request '# HOTFIX' (#388) from master into Docker
Reviewed-on: SGD-Nord/konova#388
2024-02-21 18:32:16 +01:00
f6c500b02a Merge pull request 'master' (#387) from master into Docker
Reviewed-on: SGD-Nord/konova#387
2024-02-16 10:16:43 +01:00
d702cd8716 Merge pull request 'master' (#385) from master into Docker
Reviewed-on: SGD-Nord/konova#385
2024-02-16 08:45:32 +01:00
329cdd4838 Merge pull request 'Docker_django5' (#380) from Docker_django5 into Docker
Reviewed-on: SGD-Nord/konova#380
2024-01-05 17:39:48 +01:00
1b70024a29 # Python bullseye
* adds -bullseye to base docker package to ensure backwards compatibility
2024-01-05 17:38:58 +01:00
58206853ee Django5
* changes python dependency
2024-01-05 10:05:27 +01:00
6356398c40 Merge pull request 'master' (#379) from master into Docker_django5
Reviewed-on: SGD-Nord/konova#379
2024-01-05 09:47:53 +01:00
8519922d78 Merge pull request 'Netgis map client fix' (#376) from master into Docker
Reviewed-on: SGD-Nord/konova#376
2023-12-28 15:13:11 +01:00
5ac0654fd4 Merge pull request 'master' (#375) from master into Docker
Reviewed-on: SGD-Nord/konova#375
2023-12-13 13:38:00 +01:00
6c07a81b4f Merge pull request 'master' (#372) from master into Docker
Reviewed-on: SGD-Nord/konova#372
2023-12-12 07:35:17 +01:00
ba45b4f961 Merge pull request 'HOTFIX netgis client' (#367) from master into Docker
Reviewed-on: SGD-Nord/konova#367
2023-12-07 06:44:47 +01:00
280de82a52 Merge pull request 'Hotfix map client edit errors' (#366) from master into Docker
Reviewed-on: SGD-Nord/konova#366
2023-12-05 07:29:41 +01:00
6022e2d879 Merge pull request '# Hotfix netgis client' (#365) from master into Docker
Reviewed-on: SGD-Nord/konova#365
2023-12-05 07:06:19 +01:00
1996efcc0d Docker-compose fix
* drops local volume usage in favor of copied code into container
2023-11-30 12:44:50 +01:00
80569119cb Merge branch 'master' into Docker
# Conflicts:
#	requirements.txt
2023-11-30 12:43:44 +01:00
98e71d4e8a Merge pull request 'HOTFIX' (#355) from master into Docker
Reviewed-on: SGD-Nord/konova#355
2023-11-07 16:27:11 +01:00
fec7191ac2 Merge pull request 'HOTFIX' (#354) from master into Docker
Reviewed-on: SGD-Nord/konova#354
2023-10-26 07:27:19 +02:00
9b1085f206 Merge pull request 'HOTFIX' (#353) from master into Docker
Reviewed-on: SGD-Nord/konova#353
2023-10-26 07:23:21 +02:00
b35d175a5c Merge pull request 'master' (#351) from master into Docker
Reviewed-on: SGD-Nord/konova#351
2023-10-25 10:10:14 +02:00
7f5fb022ac Merge pull request 'master' (#348) from master into Docker
Reviewed-on: SGD-Nord/konova#348
2023-09-15 13:21:38 +02:00
2d3314ab18 Merge pull request 'master' (#344) from master into Docker
Reviewed-on: SGD-Nord/konova#344
2023-08-25 15:06:32 +02:00
8b489f013d Merge pull request 'master' (#341) from master into Docker
Reviewed-on: SGD-Nord/konova#341
2023-08-09 07:30:18 +02:00
16ce5506d8 Merge pull request 'master' (#336) from master into Docker
Reviewed-on: SGD-Nord/konova#336
2023-05-17 14:40:17 +02:00
e440bf8372 Merge pull request 'master' (#333) from master into Docker
Reviewed-on: SGD-Nord/konova#333
2023-05-16 14:11:20 +02:00
607db267e6 Merge pull request 'master' (#330) from master into Docker
Reviewed-on: SGD-Nord/konova#330
2023-04-26 11:30:22 +02:00
352ca64e09 Merge pull request 'master' (#327) from master into Docker
Reviewed-on: SGD-Nord/konova#327
2023-04-19 15:25:05 +02:00
f2b735da6e Merge pull request 'master' (#324) from master into Docker
Reviewed-on: SGD-Nord/konova#324
2023-03-30 15:12:47 +02:00
6f7cfb713e Merge pull request 'master' (#321) from master into Docker
Reviewed-on: SGD-Nord/konova#321
2023-03-28 13:53:14 +02:00
103b703ee9 Merge pull request 'master' (#318) from master into Docker
Reviewed-on: SGD-Nord/konova#318
2023-03-24 07:14:34 +01:00
daf8b1dce6 Merge pull request 'master' (#316) from master into Docker
Reviewed-on: SGD-Nord/konova#316
2023-03-22 09:00:33 +01:00
c088affd74 Merge pull request 'master' (#313) from master into Docker
Reviewed-on: SGD-Nord/konova#313
2023-03-16 08:14:37 +01:00
ecc727c991 Merge pull request 'master' (#311) from master into Docker
Reviewed-on: SGD-Nord/konova#311
2023-03-13 07:00:49 +01:00
632569fa5d Merge pull request 'master' (#307) from master into Docker
Reviewed-on: SGD-Nord/konova#307
2023-02-23 15:35:07 +01:00
6c6cbb7396 Merge pull request 'HOTFIX' (#305) from master into Docker
Reviewed-on: SGD-Nord/konova#305
2023-02-23 12:03:23 +01:00
5e6bfdf77e Merge pull request 'HOTFIX' (#304) from master into Docker
Reviewed-on: SGD-Nord/konova#304
2023-02-23 10:45:57 +01:00
35e5e18b79 Merge pull request 'master' (#303) from master into Docker
Reviewed-on: SGD-Nord/konova#303
2023-02-23 10:24:38 +01:00
c0e8c6bd84 Merge pull request 'master' (#298) from master into Docker
Reviewed-on: SGD-Nord/konova#298
2023-02-21 08:07:47 +01:00
64541b76c5 Merge pull request 'master' (#295) from master into Docker
Reviewed-on: SGD-Nord/konova#295
2023-02-13 14:42:17 +01:00
f65b9262cb Merge pull request 'master' (#292) from master into Docker
Reviewed-on: SGD-Nord/konova#292
2023-02-06 15:01:50 +01:00
2765d0548e Merge pull request 'Quality Check Command enhancement' (#288) from master into Docker
Reviewed-on: SGD-Nord/konova#288
2023-02-01 14:17:21 +01:00
951f810ce5 Merge pull request 'master' (#287) from master into Docker
Reviewed-on: SGD-Nord/konova#287
2023-02-01 14:10:04 +01:00
d2c177d448 Merge pull request 'master' (#283) from master into Docker
Reviewed-on: SGD-Nord/konova#283
2022-12-22 07:56:02 +01:00
299727a7b4 Merge pull request 'master' (#279) from master into Docker
Reviewed-on: SGD-Nord/konova#279
2022-12-14 16:37:41 +01:00
b97976b2c5 Merge pull request 'master' (#276) from master into Docker
Reviewed-on: SGD-Nord/konova#276
2022-12-13 06:50:43 +01:00
20241661ff Merge pull request 'master' (#273) from master into Docker
Reviewed-on: SGD-Nord/konova#273
2022-12-09 13:02:46 +01:00
ad5c0bea67 Merge pull request 'master' (#270) from master into Docker
Reviewed-on: SGD-Nord/konova#270
2022-12-09 07:25:30 +01:00
80a44277bc Merge pull request 'Hotfix: Resubmission mail' (#265) from master into Docker
Reviewed-on: SGD-Nord/konova#265
2022-12-05 06:55:53 +01:00
5c2b5affc9 Merge pull request 'master' (#264) from master into Docker
Reviewed-on: SGD-Nord/konova#264
2022-12-05 06:10:26 +01:00
cd99743d1e Merge pull request 'master' (#261) from master into Docker
Reviewed-on: SGD-Nord/konova#261
2022-12-02 06:43:28 +01:00
b39432be1a Merge pull request 'master' (#259) from master into Docker
Reviewed-on: SGD-Nord/konova#259
2022-12-01 14:01:40 +01:00
03f9a33e54 Merge pull request 'Hotfix' (#256) from master into Docker
Reviewed-on: SGD-Nord/konova#256
2022-12-01 07:05:50 +01:00
699a9c1e76 Merge pull request 'Revert "File number public reports"' (#254) from master into Docker
Reviewed-on: SGD-Nord/konova#254
2022-11-28 13:52:34 +01:00
4dfd02291e Merge pull request 'master' (#253) from master into Docker
Reviewed-on: SGD-Nord/konova#253
2022-11-28 07:29:36 +01:00
e7ca485a88 Merge pull request 'master' (#247) from master into Docker
Reviewed-on: SGD-Nord/konova#247
2022-11-23 16:07:21 +01:00
8319cbfe17 Merge pull request 'master' (#245) from master into Docker
Reviewed-on: SGD-Nord/konova#245
2022-11-23 07:14:15 +01:00
4a023e9f10 Merge pull request 'Hotfix' (#242) from master into Docker
Reviewed-on: SGD-Nord/konova#242
2022-11-18 16:22:50 +01:00
4100f96dc6 Merge pull request 'master' (#241) from master into Docker
Reviewed-on: SGD-Nord/konova#241
2022-11-18 16:17:39 +01:00
ca24f098e4 Merge pull request 'master' (#236) from master into Docker
Reviewed-on: SGD-Nord/konova#236
2022-11-18 06:53:27 +01:00
80dcd62199 Merge pull request 'master' (#234) from master into Docker
Reviewed-on: SGD-Nord/konova#234
2022-11-17 06:55:28 +01:00
0cfd3da728 Merge pull request 'master' (#227) from master into Docker
Reviewed-on: SGD-Nord/konova#227
2022-11-14 07:23:43 +01:00
e141851a87 Merge pull request 'master' (#224) from master into Docker
Reviewed-on: SGD-Nord/konova#224
2022-10-19 07:35:31 +02:00
89ec67999b Merge pull request 'master' (#221) from master into Docker
Reviewed-on: SGD-Nord/konova#221
2022-10-12 09:02:49 +02:00
ec38daaedc Merge pull request 'master' (#219) from master into Docker
Reviewed-on: SGD-Nord/konova#219
2022-10-11 16:41:06 +02:00
45c0826a84 Merge pull request 'master' (#215) from master into Docker
Reviewed-on: SGD-Nord/konova#215
2022-10-05 11:03:03 +02:00
45a383cf85 Merge pull request 'master' (#213) from master into Docker
Reviewed-on: SGD-Nord/konova#213
2022-09-29 10:46:30 +02:00
90aff209f9 Merge pull request 'master' (#210) from master into Docker
Reviewed-on: SGD-Nord/konova#210
2022-09-28 12:28:49 +02:00
13528e91e9 Merge pull request 'master' (#207) from master into Docker
Reviewed-on: SGD-Nord/konova#207
2022-09-16 12:13:59 +02:00
04179d633c Merge pull request 'master' (#205) from master into Docker
Reviewed-on: SGD-Nord/konova#205
2022-09-16 07:24:21 +02:00
0a241305d3 Merge pull request 'Docker Update' (#199) from master into Docker
Reviewed-on: SGD-Nord/konova#199
2022-08-15 11:23:09 +02:00
31565a0bc4 Merge pull request 'Docker_tmp' (#188) from Docker_tmp into Docker
Reviewed-on: SGD-Nord/konova#188
2022-08-02 09:54:31 +02:00
af747417d3 Revert "Revert "Merge branch 'Docker' into master""
This reverts commit 1c38acea25.
2022-08-02 09:44:25 +02:00
c6606c4151 Merge pull request 'master' (#187) from master into Docker_tmp
Reviewed-on: SGD-Nord/konova#187
2022-08-02 09:43:04 +02:00
127 changed files with 4061 additions and 1456 deletions

View File

@@ -24,6 +24,7 @@ DEFAULT_FROM_EMAIL=service@ksp.de
# Proxy
PROXY=CHANGE_ME
MAP_PROXY_HOST_WHITELIST=CHANGE_ME_1,CHANGE_ME_2
GEOPORTAL_RLP_USER=CHANGE_ME
GEOPORTAL_RLP_PASSWORD=CHANGE_ME
@@ -37,6 +38,7 @@ SSO_SERVER_BASE_URL=https://login.naturschutz.rlp.de
OAUTH_CODE_VERIFIER=CHANGE_ME
OAUTH_CLIENT_ID=CHANGE_ME
OAUTH_CLIENT_SECRET=CHANGE_ME
PROPAGATION_SECRET=CHANGE_ME
# RabbitMQ
## For connections to EGON

36
Dockerfile Normal file
View File

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

View File

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

View File

@@ -51,7 +51,7 @@ class APIUserToken(models.Model):
if token_obj.valid_until is not None and token_obj.valid_until < _today:
raise PermissionError("Token validity expired")
except ObjectDoesNotExist:
raise PermissionError("Credentials invalid")
raise PermissionError("Token unknown")
return token_obj.user
@@ -155,3 +155,25 @@ class OAuthToken(UuidModel):
return user
def revoke(self) -> int:
""" Revokes the OAuth2 token of the user
(/o/revoke_token/ indeed removes the corresponding access token on provider side and invalidates the
submitted refresh token in one step)
Returns:
revocation_status_code (int): HTTP status code for revocation of refresh_token
"""
revoke_url = f"{SSO_SERVER_BASE}o/revoke_token/"
token = self.refresh_token
revocation_status_code = requests.post(
revoke_url,
data={
'token': token,
'token_type_hint': "refresh_token",
},
auth=(OAUTH_CLIENT_ID, OAUTH_CLIENT_SECRET),
).status_code
return revocation_status_code

View File

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

View File

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

View File

@@ -13,7 +13,7 @@ from django.contrib.gis.geos import GEOSGeometry
from django.core.paginator import Paginator
from django.db.models import Q
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.models import Geometry
from konova.utils.message_templates import DATA_UNSHARED
@@ -145,8 +145,8 @@ class AbstractModelAPISerializer:
if isinstance(geojson, dict):
geojson = json.dumps(geojson)
geometry = geos.fromstr(geojson)
if geometry.srid != DEFAULT_SRID_RLP:
geometry.transform(DEFAULT_SRID_RLP)
geometry = Geometry.cast_to_rlp_srid(geometry)
geometry = Geometry.cast_to_multipolygon(geometry)
return geometry
def _get_obj_from_db(self, id, user):

View File

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

View File

@@ -6,7 +6,9 @@ Created on: 21.01.22
"""
import json
from json import JSONDecodeError
from django.core.exceptions import ObjectDoesNotExist
from django.http import JsonResponse, HttpRequest
from api.utils.serializer.v1.compensation import CompensationAPISerializerV1
@@ -66,8 +68,12 @@ class AbstractAPIViewV1(AbstractAPIView):
body = request.body.decode("utf-8")
body = json.loads(body)
created_id = self.serializer.create_model_from_json(body, self.user)
except Exception as e:
return self._return_error_response(e, 500)
except (JSONDecodeError,
AssertionError,
ValueError,
PermissionError,
ObjectDoesNotExist) as e:
return self._return_error_response(e, 400)
return JsonResponse({"id": created_id})
def put(self, request: HttpRequest, id=None):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,7 @@ from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE, DATA_CHECKED_PREVIOUSLY_TEMPLATE, \
RECORDED_BLOCKS_EDIT, CHECK_STATE_RESET, FORM_INVALID, PARAMS_INVALID, IDENTIFIER_REPLACED, \
COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
COMPENSATION_ADDED_TEMPLATE, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
@login_required
@@ -103,11 +103,19 @@ def new_view(request: HttpRequest, intervention_id: str = None):
)
)
messages.success(request, COMPENSATION_ADDED_TEMPLATE.format(comp.identifier))
if geom_form.geometry_simplified:
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("compensation:detail", id=comp.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -179,11 +187,19 @@ def edit_view(request: HttpRequest, id: str):
if intervention_is_checked:
messages.info(request, CHECK_STATE_RESET)
messages.success(request, _("Compensation {} edited").format(comp.identifier))
if geom_form.geometry_simplified:
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("compensation:detail", id=comp.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -259,7 +275,7 @@ def detail_view(request: HttpRequest, id: str):
"last_checked_tooltip": last_checked_tooltip,
"geom_form": geom_form,
"parcels": parcels,
"has_access": is_data_shared,
"is_entry_shared": is_data_shared,
"actions": actions,
"before_states": before_states,
"after_states": after_states,

View File

@@ -12,11 +12,12 @@ from django.utils.translation import gettext_lazy as _
from compensation.models import Compensation
from konova.contexts import BaseContext
from konova.decorators import uuid_required
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
@uuid_required
def report_view(request: HttpRequest, id: str):
""" Renders the public report view
@@ -67,7 +68,7 @@ def report_view(request: HttpRequest, id: str):
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"has_access": False, # disables action buttons during rendering
"is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,

View File

@@ -23,7 +23,7 @@ from konova.forms import SimpleGeomForm
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import CANCEL_ACC_RECORDED_OR_DEDUCTED, RECORDED_BLOCKS_EDIT, FORM_INVALID, \
IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
@login_required
@@ -84,11 +84,19 @@ def new_view(request: HttpRequest):
)
)
messages.success(request, _("Eco-Account {} added").format(acc.identifier))
if geom_form.geometry_simplified:
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("compensation:acc:detail", id=acc.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -156,11 +164,19 @@ def edit_view(request: HttpRequest, id: str):
# The data form takes the geom form for processing, as well as the performing user
acc = data_form.save(request.user, geom_form)
messages.success(request, _("Eco-Account {} edited").format(acc.identifier))
if geom_form.geometry_simplified:
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("compensation:acc:detail", id=acc.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -237,7 +253,7 @@ def detail_view(request: HttpRequest, id: str):
"obj": acc,
"geom_form": geom_form,
"parcels": parcels,
"has_access": is_data_shared,
"is_entry_shared": is_data_shared,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,

View File

@@ -12,11 +12,13 @@ from django.utils.translation import gettext_lazy as _
from compensation.models import EcoAccount
from konova.contexts import BaseContext
from konova.decorators import uuid_required
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
@uuid_required
def report_view(request: HttpRequest, id: str):
""" Renders the public report view
@@ -73,7 +75,7 @@ def report_view(request: HttpRequest, id: str):
"img": qrcode_img_lanis,
"url": qrcode_lanis_url,
},
"has_access": False, # disables action buttons during rendering
"is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,

23
docker-compose.yml Normal file
View File

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

27
docker-entrypoint.sh Executable file
View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -46,7 +46,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
post_data = {
"identifier": test_id,
"title": test_title,
"geom": geom_json,
"output": geom_json,
"conservation_office": test_conservation_office.id
}
self.client_user.post(new_url, post_data)
@@ -84,7 +84,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string()
new_geometry = MultiPolygon(srid=4326) # Create an empty geometry
new_geometry = self.create_dummy_geometry() # Create an empty geometry
test_conservation_office = self.get_conservation_office_code()
check_on_elements = {
@@ -99,7 +99,7 @@ class EmaWorkflowTestCase(BaseWorkflowTestCase):
"identifier": new_identifier,
"title": new_title,
"comment": new_comment,
"geom": new_geometry.geojson,
"output": self.create_geojson(new_geometry),
"conservation_office": test_conservation_office.id
}
self.client_user.post(url, post_data)

View File

@@ -48,7 +48,7 @@ class NewEmaFormTestCase(BaseTestCase):
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
"output": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)
@@ -116,7 +116,7 @@ class EditEmaFormTestCase(BaseTestCase):
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
"output": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)

View File

@@ -24,7 +24,7 @@ from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import RECORDED_BLOCKS_EDIT, IDENTIFIER_REPLACED, FORM_INVALID, \
DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, GEOMETRIES_IGNORED_TEMPLATE
@login_required
@@ -84,11 +84,17 @@ def new_view(request: HttpRequest):
)
)
messages.success(request, _("EMA {} added").format(ema.identifier))
if geom_form.geometry_simplified:
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("ema:detail", id=ema.id)
else:
@@ -142,7 +148,7 @@ def detail_view(request: HttpRequest, id: str):
geom_form = SimpleGeomForm(instance=ema)
parcels = ema.get_underlying_parcels()
_user = request.user
is_data_shared = ema.is_shared_with(_user)
is_entry_shared = ema.is_shared_with(_user)
# Order states according to surface
before_states = ema.before_states.all().order_by("-surface")
@@ -167,7 +173,7 @@ def detail_view(request: HttpRequest, id: str):
"obj": ema,
"geom_form": geom_form,
"parcels": parcels,
"has_access": is_data_shared,
"is_entry_shared": is_entry_shared,
"before_states": before_states,
"after_states": after_states,
"sum_before_states": sum_before_states,
@@ -215,11 +221,19 @@ def edit_view(request: HttpRequest, id: str):
# The data form takes the geom form for processing, as well as the performing user
ema = data_form.save(request.user, geom_form)
messages.success(request, _("EMA {} edited").format(ema.identifier))
if geom_form.geometry_simplified:
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("ema:detail", id=ema.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)

View File

@@ -12,11 +12,12 @@ from django.utils.translation import gettext_lazy as _
from ema.models import Ema
from konova.contexts import BaseContext
from konova.decorators import uuid_required
from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code
@uuid_required
def report_view(request:HttpRequest, id: str):
""" Renders the public report view
@@ -67,7 +68,7 @@ def report_view(request:HttpRequest, id: str):
"img": qrcode_img_lanis,
"url": qrcode_lanis_url
},
"has_access": False, # disables action buttons during rendering
"is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states,
"after_states": after_states,
"geom_form": geom_form,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -62,7 +62,7 @@ class NewInterventionFormTestCase(BaseTestCase):
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
"output": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)
@@ -104,7 +104,7 @@ class EditInterventionFormTestCase(NewInterventionFormTestCase):
)
geom_form_data = json.loads(geom_form_data)
geom_form_data = {
"geom": json.dumps(geom_form_data)
"output": json.dumps(geom_form_data)
}
geom_form = SimpleGeomForm(geom_form_data)
@@ -124,7 +124,7 @@ class EditInterventionFormTestCase(NewInterventionFormTestCase):
self.assertIsNotNone(obj.responsible.handler)
self.assertEqual(obj.title, data["title"])
self.assertEqual(obj.comment, data["comment"])
self.assertTrue(test_geom.equals_exact(obj.geometry.geom, 0.000001))
self.assert_equal_geometries(test_geom, obj.geometry.geom)
self.assertEqual(obj.legal.binding_date, today)
self.assertEqual(obj.legal.registration_date, today)

View File

@@ -23,7 +23,8 @@ from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.message_templates import DATA_CHECKED_PREVIOUSLY_TEMPLATE, RECORDED_BLOCKS_EDIT, \
CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED
CHECK_STATE_RESET, FORM_INVALID, IDENTIFIER_REPLACED, DO_NOT_FORGET_TO_SHARE, GEOMETRY_SIMPLIFIED, \
GEOMETRIES_IGNORED_TEMPLATE
@login_required
@@ -88,11 +89,19 @@ def new_view(request: HttpRequest):
)
)
messages.success(request, _("Intervention {} added").format(intervention.identifier))
if geom_form.geometry_simplified:
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("intervention:detail", id=intervention.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)
@@ -185,7 +194,7 @@ def detail_view(request: HttpRequest, id: str):
"last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip,
"compensations": compensations,
"has_access": is_data_shared,
"is_entry_shared": is_data_shared,
"geom_form": geom_form,
"is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP),
@@ -236,11 +245,19 @@ def edit_view(request: HttpRequest, id: str):
messages.success(request, _("Intervention {} edited").format(intervention.identifier))
if intervention_is_checked:
messages.info(request, CHECK_STATE_RESET)
if geom_form.geometry_simplified:
if geom_form.has_geometry_simplified():
messages.info(
request,
GEOMETRY_SIMPLIFIED
)
num_ignored_geometries = geom_form.get_num_geometries_ignored()
if num_ignored_geometries > 0:
messages.info(
request,
GEOMETRIES_IGNORED_TEMPLATE.format(num_ignored_geometries)
)
return redirect("intervention:detail", id=intervention.id)
else:
messages.error(request, FORM_INVALID, extra_tags="danger",)

View File

@@ -11,7 +11,7 @@ from uuid import UUID
from bootstrap_modal_forms.mixins import is_ajax
from django.contrib import messages
from django.http import Http404
from django.core.exceptions import BadRequest
from django.shortcuts import redirect, get_object_or_404, render
from django.urls import reverse
from django.utils.translation import gettext_lazy as _
@@ -185,7 +185,7 @@ def uuid_required(function):
try:
uuid = UUID(uuid)
except ValueError:
raise Http404(
raise BadRequest(
"Invalid UUID"
)
return function(request, *args, **kwargs)

View File

@@ -0,0 +1,55 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.24
"""
import django_filters
from django import forms
from django.db.models import QuerySet, Q
from django.utils.translation import gettext_lazy as _
class UserLoggedTableFilterMixin(django_filters.FilterSet):
ul = django_filters.CharFilter(
method="filter_user_log",
label=_(""),
label_suffix=_(""),
widget=forms.TextInput(
attrs={
"placeholder": _("Logged user"),
"title": _("Search for entries where this person has been participated according to log history"),
"class": "form-control",
}
),
)
class Meta:
abstract = True
def filter_user_log(self, queryset, name, value) -> QuerySet:
""" Filters queryset depending on value of input
Args:
queryset (QuerySet): Incoming (prefiltered) queryset
name (str): Name of input field
value (str): Value of input field
Returns:
"""
value = value.replace(",", " ")
value = value.strip()
values = value.split(" ")
q = Q()
for val in values:
q &= (
Q(log__user__username__icontains=val) |
Q(log__user__first_name__icontains=val) |
Q(log__user__last_name__icontains=val)
)
queryset = queryset.filter(q)
return queryset

View File

@@ -14,6 +14,7 @@ from konova.filters.mixins.office import ConservationOfficeTableFilterMixin, Reg
from konova.filters.mixins.record import RecordableTableFilterMixin
from konova.filters.mixins.self_created import SelfCreatedTableFilterMixin
from konova.filters.mixins.share import ShareableTableFilterMixin
from konova.filters.mixins.user_log import UserLoggedTableFilterMixin
class AbstractTableFilter(django_filters.FilterSet):
@@ -40,7 +41,8 @@ class SelectionTableFilter(RegistrationOfficeTableFilterMixin,
class QueryTableFilter(KeywordTableFilterMixin,
FileNumberTableFilterMixin,
GeoReferencedTableFilterMixin):
GeoReferencedTableFilterMixin,
UserLoggedTableFilterMixin):
""" TableFilter holding different filter options for query related filtering
"""

View File

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

View File

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

View File

@@ -0,0 +1,88 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.01.22
"""
import datetime
from django.contrib.gis.db.models.functions import Area
from konova.management.commands.setup import BaseKonovaCommand
from konova.models import Geometry, ParcelIntersection
class Command(BaseKonovaCommand):
help = "Recalculates parcels for entries with geometry but missing parcel information"
def add_arguments(self, parser):
parser.add_argument(
"--force-all",
action="store_true",
default=False,
help="If Attribute set, all entries parcels will be recalculated"
)
def handle(self, *args, **options):
try:
self.recalculate_parcels(options)
except KeyboardInterrupt:
self._break_line()
exit(-1)
def recalculate_parcels(self, options: dict):
force_all = options.get("force_all", False)
geometry_objects = Geometry.objects.filter(
geom__isempty=False,
).exclude(
geom=None
)
if not force_all:
# Fetch all intersections
intersection_objs = ParcelIntersection.objects.filter(
geometry__in=geometry_objects
)
# Just take the geometry ids, which seem to have intersections
geom_with_intersection_ids = intersection_objs.values_list(
"geometry__id",
flat=True
)
# ... and resolve into Geometry objects again ...
intersected_geom_objs = Geometry.objects.filter(
id__in=geom_with_intersection_ids
)
# ... to be able to use the way more efficient difference() function ...
geometry_objects_ids = geometry_objects.difference(intersected_geom_objs).values_list("id", flat=True)
# ... so we can resolve these into proper Geometry objects again for further annotation usage
geometry_objects = Geometry.objects.filter(id__in=geometry_objects_ids)
self._write_warning("=== Update parcels and districts ===")
# Order geometries by size to process smaller once at first
geometries = geometry_objects.annotate(
area=Area("geom")
).order_by(
'area'
)
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
i = 0
num_geoms = geometries.count()
geoms_with_errors = {}
for geometry in geometries:
self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
try:
geometry.update_parcels()
self._write_warning(f"--- Processed {geometry.get_underlying_parcels().count()} underlying parcels")
except Exception as e:
geoms_with_errors[geometry.id] = str(e)
i += 1
self._write_warning(f"--- {i}/{num_geoms} processed")
self._write_success("Updating parcels done!")
for key, val in geoms_with_errors.items():
self._write_error(f" Error on {key}: {val}")
self._write_success(f"{num_geoms - len(geoms_with_errors)} geometries successfuly recalculated!")
self._break_line()

View File

@@ -1,51 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 19.08.21
"""
from django.core.management import BaseCommand
from intervention.models import Intervention
class Command(BaseCommand):
help = "Performs test on collisions using the identifier generation"
def handle(self, *args, **options):
identifiers = {}
max_iterations = 100000
try:
collisions = 0
len_ids = len(identifiers)
while len_ids < max_iterations:
tmp_intervention = Intervention()
_id = tmp_intervention.generate_new_identifier()
len_ids = len(identifiers)
if _id not in identifiers:
if len_ids % (max_iterations/5) == 0:
print(len_ids)
identifiers[_id] = None
else:
collisions += 1
print("+++ Collision after {} identifiers +++".format(len_ids))
except KeyboardInterrupt:
self._break_line()
exit(-1)
print(
"\n{} collisions in {} identifiers; Collision rate {}%".format(
collisions,
len_ids,
(collisions / len_ids)*100,
)
)
def _break_line(self):
""" Simply prints a line break
Returns:
"""
self.stdout.write("\n")

View File

@@ -1,54 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 04.01.22
"""
import datetime
from django.contrib.gis.db.models.functions import Area
from konova.management.commands.setup import BaseKonovaCommand
from konova.models import Geometry, Parcel, District
class Command(BaseKonovaCommand):
help = "Checks the database' sanity and removes unused entries"
def handle(self, *args, **options):
try:
self.update_all_parcels()
except KeyboardInterrupt:
self._break_line()
exit(-1)
def update_all_parcels(self):
num_parcels_before = Parcel.objects.count()
num_districts_before = District.objects.count()
self._write_warning("=== Update parcels and districts ===")
# Order geometries by size to process smaller once at first
geometries = Geometry.objects.all().exclude(
geom=None
).annotate(area=Area("geom")).order_by(
'area'
)
self._write_warning(f"Process parcels for {geometries.count()} geometry entries now ...")
i = 0
num_geoms = geometries.count()
for geometry in geometries:
self._write_warning(f"--- {datetime.datetime.now()} Process {geometry.id} now ...")
geometry.update_parcels()
self._write_warning(f"--- Processed {geometry.get_underlying_parcels().count()} underlying parcels")
i += 1
self._write_warning(f"--- {i}/{num_geoms} processed")
num_parcels_after = Parcel.objects.count()
num_districts_after = District.objects.count()
if num_parcels_after != num_parcels_before:
self._write_error(f"Parcels have changed: {num_parcels_before} to {num_parcels_after} entries. You should run the sanitize command.")
if num_districts_after != num_districts_before:
self._write_error(f"Districts have changed: {num_districts_before} to {num_districts_after} entries. You should run the sanitize command.")
self._write_success("Updating parcels done!")
self._break_line()

View File

@@ -8,9 +8,11 @@ Created on: 15.11.21
import json
from django.contrib.gis.db.models import MultiPolygonField
from django.core.exceptions import ObjectDoesNotExist
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.db import models, transaction
from django.db.models import Q
from django.utils import timezone
from django.contrib.gis.geos import MultiPolygon
from konova.models import BaseResource, UuidModel
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
@@ -108,17 +110,26 @@ class Geometry(BaseResource):
objs (list): The list of objects
"""
objs = []
sets = [
# Some related data sets can be processed rather easily
regular_sets = [
self.intervention_set,
self.compensation_set,
self.ema_set,
self.ecoaccount_set,
]
for _set in sets:
for _set in regular_sets:
set_objs = _set.filter(
deleted=None
)
objs += set_objs
# ... but we need a special treatment for compensations, since they can be deleted directly OR inherit their
# de-facto-deleted status from their deleted parent intervention
comp_objs = self.compensation_set.filter(
Q(deleted=None) & Q(intervention__deleted=None)
)
objs += comp_objs
return objs
def get_data_object(self):
@@ -223,6 +234,17 @@ class Geometry(BaseResource):
)
parcel_obj.updated_on = _now
parcels_to_update.append(parcel_obj)
except MultipleObjectsReturned:
parcel_obj = Parcel.make_unique(
district=district,
municipal=municipal,
parcel_group=parcel_group,
flr=flr_val,
flrstck_nnr=flrstck_nnr,
flrstck_zhlr=flrstck_zhlr,
)
parcel_obj.updated_on = _now
parcels_to_update.append(parcel_obj)
except ObjectDoesNotExist:
# If not existing, create object but do not commit, yet
parcel_obj = Parcel(
@@ -331,6 +353,7 @@ class Geometry(BaseResource):
{
"type": "Feature",
"geometry": json.loads(p.json),
"properties": {},
}
for p in polygons
]
@@ -366,13 +389,45 @@ class Geometry(BaseResource):
diff = geom_envelope - self.geom
if diff.area == 0:
ratio = 1
complexity_factor = 1
else:
ratio = self.geom.area / diff.area
complexity_factor = self.geom.area / diff.area
complexity_factor = 1 - ratio
return complexity_factor
@staticmethod
def cast_to_multipolygon(input_geom):
""" If input_geom is not a MultiPolygon, cast to MultiPolygon
Args:
input_geom ():
Returns:
output_geom
"""
output_geom = input_geom
if not isinstance(input_geom, MultiPolygon):
try:
output_geom = MultiPolygon(input_geom, srid=DEFAULT_SRID_RLP)
except TypeError as e:
raise AssertionError(f"Only (Multi)Polygon allowed! Could not convert {input_geom.geom_type} to MultiPolygon")
return output_geom
@staticmethod
def cast_to_rlp_srid(input_geom):
""" If input_geom is not of RLP SRID (25832), cast to RLP SRID
Args:
input_geom ():
Returns:
output_geom
"""
output_geom = input_geom
if output_geom.srid != DEFAULT_SRID_RLP:
output_geom.transform(DEFAULT_SRID_RLP)
return output_geom
class GeometryConflict(UuidModel):
"""

View File

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

View File

@@ -5,7 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.12.21
"""
from django.db import models
from django.db import models, transaction
from konova.models import UuidModel
@@ -158,6 +158,46 @@ class Parcel(UuidModel):
def __str__(self):
return f"{self.parcel_group} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
@classmethod
def make_unique(cls, **kwargs):
""" Checks for duplicates of a Parcel, choose a (now) unique one,
repairs relations for ParcelIntersection and removes duplicates.
Args:
**kwargs ():
Returns:
unique_true (Parcel): The new unique 'true one'
"""
parcel_objs = Parcel.objects.filter(**kwargs)
if not parcel_objs.exists():
return None
# Get one of the found parcels and use it as new 'true one'
unique_parcel = parcel_objs.first()
# separate it from the rest
parcel_objs = parcel_objs.exclude(id=unique_parcel.id)
if not parcel_objs.exists():
# There are no duplicates - all good, just return
return unique_parcel
# Fetch existing intersections, which still point on the duplicated parcels
intersection_objs = ParcelIntersection.objects.filter(
parcel__in=parcel_objs
)
# Change each intersection, so they point on the 'true one' parcel from now on
for intersection in intersection_objs:
intersection.parcel = unique_parcel
intersection.save()
# Remove the duplicated parcels
parcel_objs.delete()
return unique_parcel
class ParcelIntersection(UuidModel):
"""

View File

@@ -278,4 +278,18 @@ Similar to bootstraps 'shadow-lg'
}
.alert{
margin-bottom: 0 !important;
}
/*
Overwrites netgis.css attributes
*/
.netgis-gradient-a{
/*
Overwrites gradient used on default css of netgis map client
*/
background: var(--rlp-red) !important;
}
.netgis-menu{
z-index: 1 !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

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

View File

@@ -66,7 +66,6 @@ INSTALLED_APPS = [
'django.contrib.staticfiles',
'django.contrib.gis',
'django.contrib.humanize',
'simple_sso.sso_server',
'django_tables2',
'bootstrap_modal_forms',
'fontawesome_5',
@@ -189,15 +188,13 @@ STATIC_ROOT = os.path.join(BASE_DIR, "static")
STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'konova/static'),
os.path.join(BASE_DIR, 'templates/map/client'), # NETGIS map client files
os.path.join(BASE_DIR, 'templates/map/client/libs'), # NETGIS map client files
]
# EMAIL (see https://docs.djangoproject.com/en/dev/topics/email/)
# CHANGE_ME !!! ONLY FOR DEVELOPMENT !!!
if DEBUG:
# ONLY FOR DEVELOPMENT NEEDED
EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location
EMAIL_FILE_PATH = '/tmp/app-messages'
DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL") # The default email address for the 'from' element
SERVER_EMAIL = DEFAULT_FROM_EMAIL # The default email sender address, which is used by Django to send errors via mail

View File

@@ -5,6 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 31.01.22
"""
from konova.sub_settings.django_settings import env
# MAPS
DEFAULT_LAT = 50.00
@@ -28,3 +29,6 @@ LANIS_ZOOM_LUT = {
1000: 30,
500: 31,
}
MAP_PROXY_HOST_WHITELIST = env.list("MAP_PROXY_HOST_WHITELIST")
i = 0

View File

@@ -16,3 +16,5 @@ OAUTH_CODE_VERIFIER = env("OAUTH_CODE_VERIFIER")
OAUTH_CLIENT_ID = env("OAUTH_CLIENT_ID")
OAUTH_CLIENT_SECRET = env("OAUTH_CLIENT_SECRET")
PROPAGATION_SECRET = env("PROPAGATION_SECRET")

View File

@@ -124,6 +124,7 @@ class GeometryTestCase(BaseTestCase):
{
"type": "Feature",
"geometry": json.loads(p.json),
"properties": {}
}
for p in polygons
]

View File

@@ -469,7 +469,7 @@ class BaseTestCase(TestCase):
eco_account.save()
return eco_account
def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon, tolerance = 0.001):
def assert_equal_geometries(self, geom1: MultiPolygon, geom2: MultiPolygon, tolerance=0.001):
""" Assert for geometries to be equal
Transforms the geometries to matching srids before checking
@@ -491,7 +491,10 @@ class BaseTestCase(TestCase):
# transformation from one coordinate system into the other, which is valid
geom1.transform(geom2.srid)
geom2.transform(geom1.srid)
self.assertTrue(geom1.equals_exact(geom2, tolerance) or geom2.equals_exact(geom1, tolerance))
self.assertTrue(
geom1.equals_exact(geom2, tolerance=tolerance),
msg=f"Difference is {abs(geom1.area - geom2.area)} with {geom1.area} and {geom2.area} in a tolerance of {tolerance}"
)
class BaseViewTestCase(BaseTestCase):

View File

@@ -42,5 +42,6 @@ urlpatterns = [
path('client/proxy/wfs', ClientProxyParcelWFS.as_view(), name="client-proxy-wfs"),
]
handler400 = "konova.views.error.get_400_view"
handler404 = "konova.views.error.get_404_view"
handler500 = "konova.views.error.get_500_view"

View File

@@ -5,6 +5,9 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 11.12.23
"""
import json
from json import JSONDecodeError
from django.views.debug import ExceptionReporter
@@ -30,7 +33,7 @@ class KonovaExceptionReporter(ExceptionReporter):
"""
whitelist = [
"is_email",
"unicdoe_hint",
"unicode_hint",
"frames",
"request",
"user_str",
@@ -39,6 +42,8 @@ class KonovaExceptionReporter(ExceptionReporter):
"raising_view_name",
"exception_type",
"exception_value",
"filtered_GET_items",
"filtered_POST_items",
]
clean_data = dict()
for entry in whitelist:
@@ -56,7 +61,28 @@ class KonovaExceptionReporter(ExceptionReporter):
"""
tb_data = super().get_traceback_data()
return_data = tb_data
if self.is_email:
tb_data = self._filter_traceback_data(tb_data)
filtered_data = dict()
filtered_data.update(self._filter_traceback_data(tb_data))
filtered_data.update(self._filter_POST_body(tb_data))
return_data = filtered_data
return return_data
return tb_data
def _filter_POST_body(self, tb_data: dict):
""" Filters POST body from traceback data
"""
post_data = tb_data.get("request", None)
if post_data:
post_data = post_data.body
try:
post_data = json.loads(post_data)
except JSONDecodeError:
pass
post_data = {
"filtered_POST_items": [
("body", post_data),
]
}
return post_data

View File

@@ -83,6 +83,7 @@ EDITED_GENERAL_DATA = _("Edited general data")
# Geometry
GEOMETRY_CONFLICT_WITH_TEMPLATE = _("Geometry conflict detected with {}")
GEOMETRY_SIMPLIFIED = _("The geometry contained more than {} vertices. It had to be simplified to match the allowed limit of {} vertices.").format(GEOM_MAX_VERTICES, GEOM_MAX_VERTICES)
GEOMETRIES_IGNORED_TEMPLATE = _("The geometry contained {} parts which have been detected as invalid (e.g. too small to be valid). These parts have been removed. Please check the stored geometry.")
# INTERVENTION
INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations")
@@ -91,3 +92,6 @@ INTERVENTION_HAS_REVOCATIONS_TEMPLATE = _("This intervention has {} revocations"
DATA_CHECKED_ON_TEMPLATE = _("Checked on {} by {}")
DATA_CHECKED_PREVIOUSLY_TEMPLATE = _("Data has changed since last check on {} by {}")
DATA_IS_UNCHECKED = _("Current data not checked yet")
# API TOKEN SETTINGS
NEW_API_TOKEN_GENERATED = _("New token generated. Administrators need to validate.")

View File

@@ -1,78 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.08.21
"""
from collections import Iterable
import requests
from user.models import User
from django.utils.translation import gettext_lazy as _
from konova.settings import SSO_SERVER_BASE, SSO_PUBLIC_KEY, PROXIES
from konova.sub_settings.context_settings import BASE_TITLE_SHORT
class Messenger:
""" Used to send messages to the SSO server.
Messages can be seen by the user the next time they login on their SSO dashboard.
Documentation for SSO Server-Client communication can be found here:
https://git.naturschutz.rlp.de/SGD-Nord/arnova/wiki/Messages
"""
server_url = "{}communication/message/".format(SSO_SERVER_BASE)
def __init__(self, users: Iterable, subject: str = None, body: str = None, type: str = None):
self.users = users
self.msg_subject = subject
self.msg_body = body
self.msg_type = type
def send(self):
""" Sends a message
"""
if self.msg_body is None or len(self.msg_body) == 0:
raise AttributeError("No message body set")
headers = {
"x-services-public-key": SSO_PUBLIC_KEY
}
for user in self.users:
data = {
"type": self.msg_type,
"sender": BASE_TITLE_SHORT,
"receiver": user.username,
"subject": self.msg_subject,
"body": self.msg_body,
}
requests.post(
self.server_url,
data=data,
headers=headers,
proxies=PROXIES
)
def send_object_checked(self, obj_identifier: str, performing_user: User, detail_view_url: str = ""):
""" Wraps sending of a message related to the checking of an object, like an intervention
Args:
obj_identifier (str): The object's identifier (e.g. 'EIV-123'
performing_user (User): The user who performed the checking
detail_view_url (str): If a direct link to the object shall be added to the message, it can be provided here
Returns:
"""
self.msg_subject = _("{} checked").format(obj_identifier)
if len(detail_view_url) > 0:
detail_view_url = _('<a href="{}">Check it out</a>').format(detail_view_url)
self.msg_body = _("{} has been checked successfully by user {}! {}").format(
obj_identifier,
performing_user.username,
detail_view_url
)
self.send()

View File

@@ -11,6 +11,7 @@ from json import JSONDecodeError
import requests
from konova.sub_settings import schneider_settings
from konova.sub_settings.lanis_settings import DEFAULT_SRID
from konova.sub_settings.proxy_settings import PROXIES
@@ -34,6 +35,7 @@ class ParcelFetcher:
if geom.area < buffer_threshold:
# Fallback for malicious geometries which are way too small and would disappear on negative buffering
geom = geometry.geom
geom.transform(DEFAULT_SRID)
self.geojson = geom.ewkt
self.results = []
@@ -55,11 +57,11 @@ class ParcelFetcher:
content = json.loads(response.content.decode("utf-8"))
except JSONDecodeError:
content = {}
next = content.get("next", None)
_next = content.get("next", None)
fetched_parcels = content.get("results", [])
self.results += fetched_parcels
if next:
self.get_parcels(next)
if _next:
self.get_parcels(_next)
return self.results

View File

@@ -216,11 +216,11 @@ class TableRenderMixin:
"""
html = ""
has_access = record.is_shared_with(self.user)
is_entry_shared = record.is_shared_with(self.user)
html += self.render_icn(
tooltip=_("Full access granted") if has_access else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if has_access else "far fa-edit",
tooltip=_("Full access granted") if is_entry_shared else _("Access not granted"),
icn_class="fas fa-edit rlp-r-inv" if is_entry_shared else "far fa-edit",
)
return format_html(html)

View File

@@ -25,6 +25,20 @@ def get_404_view(request: HttpRequest, exception=None):
return render(request, "404.html", context, status=404)
def get_400_view(request: HttpRequest, exception=None):
""" Returns a 400 handling view
Args:
request ():
exception ():
Returns:
"""
context = BaseContext.context
return render(request, "400.html", context, status=400)
def get_500_view(request: HttpRequest):
""" Returns a 404 handling view

View File

@@ -5,7 +5,6 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 19.08.22
"""
from django.contrib.auth.mixins import LoginRequiredMixin
from django.contrib.gis.geos import MultiPolygon
from django.http import HttpResponse, HttpRequest
from django.shortcuts import get_object_or_404

View File

@@ -24,5 +24,13 @@ class LogoutView(View):
Returns:
A redirect
"""
user = request.user
try:
oauth_token = user.oauth_token
if oauth_token:
oauth_token.revoke()
except AttributeError:
pass
logout(request)
return redirect(SSO_SERVER_BASE)

View File

@@ -9,15 +9,16 @@ import json
from json import JSONDecodeError
import requests
import urllib3.util
from django.contrib.auth.decorators import login_required
from django.http import JsonResponse, HttpRequest
from django.http import JsonResponse, HttpRequest, HttpResponse
from django.utils.decorators import method_decorator
from django.utils.http import urlencode
from django.views import View
from django.utils.translation import gettext_lazy as _
from requests.auth import HTTPDigestAuth
from konova.sub_settings.lanis_settings import MAP_PROXY_HOST_WHITELIST
from konova.sub_settings.proxy_settings import PROXIES, GEOPORTAL_RLP_USER, GEOPORTAL_RLP_PASSWORD
@@ -32,6 +33,13 @@ class BaseClientProxyView(View):
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def _check_with_whitelist(self, url):
parsed_url = urllib3.util.parse_url(url)
parsed_url_host = parsed_url.host
whitelist = set(MAP_PROXY_HOST_WHITELIST)
is_allowed = parsed_url_host in whitelist
return is_allowed
def perform_url_call(self, url, headers={}, auth=None):
""" Generic proxied call
@@ -51,7 +59,10 @@ class BaseClientProxyView(View):
)
content = response.content
if isinstance(content, bytes):
content = content.decode("utf-8")
try:
content = content.decode("utf-8")
except UnicodeDecodeError:
content = f"Can not decode content of {url}"
return content, response.status_code
@@ -59,25 +70,13 @@ class ClientProxyParcelSearch(BaseClientProxyView):
def get(self, request: HttpRequest):
url = request.META.get("QUERY_STRING")
content, response_code = self.perform_url_call(url)
try:
body = json.loads(content)
except JSONDecodeError as e:
body = {
"totalResultsCount": -1,
"geonames": [
{
"title": _("The external service is currently unavailable.<br>Please try again in a few moments...")
}
]
}
if response_code != 200:
return JsonResponse({
"status_code": response_code,
"content": body,
})
return JsonResponse(body)
is_url_allowed = self._check_with_whitelist(url)
if not is_url_allowed:
raise PermissionError(f"Proxied url '{url}' is not allowed!")
content, response_code = self.perform_url_call(url)
return HttpResponse(content)
class ClientProxyParcelWFS(BaseClientProxyView):

View File

@@ -115,10 +115,10 @@ class OAuthCallbackView(View):
if status_code_invalid:
raise RuntimeError(f"OAuth access token could not be fetched: {access_code_response.text}")
oauth_access_token = OAuthToken.from_access_token_response(access_code_response_body, received_on)
oauth_access_token.save()
user = oauth_access_token.update_and_get_user()
user.oauth_replace_token(oauth_access_token)
oauth_token = OAuthToken.from_access_token_response(access_code_response_body, received_on)
oauth_token.save()
user = oauth_token.update_and_get_user()
user.oauth_replace_token(oauth_token)
login(request, user)
return redirect("home")

Binary file not shown.

View File

@@ -29,6 +29,7 @@
#: konova/filters/mixins/office.py:25 konova/filters/mixins/office.py:56
#: konova/filters/mixins/office.py:57 konova/filters/mixins/record.py:23
#: konova/filters/mixins/self_created.py:24 konova/filters/mixins/share.py:23
#: konova/filters/mixins/user_log.py:17 konova/filters/mixins/user_log.py:18
#: konova/forms/geometry_form.py:32 konova/forms/modals/document_form.py:26
#: konova/forms/modals/document_form.py:36
#: konova/forms/modals/document_form.py:50
@@ -37,13 +38,14 @@
#: konova/forms/modals/remove_form.py:23
#: konova/forms/modals/resubmission_form.py:22
#: konova/forms/modals/resubmission_form.py:38 konova/forms/remove_form.py:25
#: konova/tests/unit/test_forms.py:59 user/forms/user.py:39
#: konova/tests/unit/test_forms.py:59 user/forms/modals/api_token.py:17
#: user/forms/user.py:39
#, fuzzy
msgid ""
msgstr ""
"Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2024-02-16 09:49+0100\n"
"POT-Creation-Date: 2025-10-15 09:11+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
@@ -259,7 +261,7 @@ msgstr ""
#: analysis/templates/analysis/reports/includes/eco_account/deductions.html:14
#: analysis/templates/analysis/reports/includes/eco_account/deductions.html:16
#: compensation/forms/modals/state.py:58
#: compensation/forms/modals/state.py:59
#: compensation/templates/compensation/detail/compensation/includes/states-after.html:36
#: compensation/templates/compensation/detail/compensation/includes/states-before.html:36
#: compensation/templates/compensation/detail/eco_account/includes/states-after.html:36
@@ -372,7 +374,6 @@ msgid "Identifier"
msgstr "Kennung"
#: compensation/forms/compensation.py:33 intervention/forms/intervention.py:33
#: user/forms/user.py:77
msgid "Generated automatically - not editable"
msgstr "Automatisch generiert - nicht bearbeitbar"
@@ -447,7 +448,7 @@ msgid "Select the intervention for which this compensation compensates"
msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist"
#: compensation/forms/compensation.py:114
#: compensation/views/compensation/compensation.py:119
#: compensation/views/compensation/compensation.py:120
msgid "New compensation"
msgstr "Neue Kompensation"
@@ -474,7 +475,7 @@ msgid "When did the parties agree on this?"
msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?"
#: compensation/forms/eco_account.py:72
#: compensation/views/eco_account/eco_account.py:100
#: compensation/views/eco_account/eco_account.py:101
msgid "New Eco-Account"
msgstr "Neues Ökokonto"
@@ -696,46 +697,46 @@ msgid "If there is no date you can enter, please explain why."
msgstr "Falls Sie kein Datum angeben können, erklären Sie bitte weshalb."
#: compensation/forms/modals/payment.py:108
#: intervention/templates/intervention/detail/includes/payments.html:59
#: intervention/templates/intervention/detail/includes/payments.html:67
msgid "Edit payment"
msgstr "Zahlung bearbeiten"
#: compensation/forms/modals/state.py:32
#: compensation/forms/modals/state.py:33
msgid "Biotope Type"
msgstr "Biotoptyp"
#: compensation/forms/modals/state.py:35
#: compensation/forms/modals/state.py:36
msgid "Select the biotope type"
msgstr "Biotoptyp wählen"
#: compensation/forms/modals/state.py:39 compensation/forms/modals/state.py:51
#: compensation/forms/modals/state.py:40 compensation/forms/modals/state.py:52
msgid "Biotope additional type"
msgstr "Zusatzbezeichnung"
#: compensation/forms/modals/state.py:42
#: compensation/forms/modals/state.py:43
msgid "Select an additional biotope type"
msgstr "Zusatzbezeichnung wählen"
#: compensation/forms/modals/state.py:61
#: compensation/forms/modals/state.py:62
#: intervention/forms/modals/deduction.py:49
msgid "in m²"
msgstr ""
#: compensation/forms/modals/state.py:72
#: compensation/forms/modals/state.py:73
#: compensation/tests/compensation/unit/test_forms.py:175
msgid "New state"
msgstr "Neuer Zustand"
#: compensation/forms/modals/state.py:73
#: compensation/forms/modals/state.py:74
#: compensation/tests/compensation/unit/test_forms.py:176
msgid "Insert data for the new state"
msgstr "Geben Sie die Daten des neuen Zustandes ein"
#: compensation/forms/modals/state.py:90 konova/forms/modals/base_form.py:32
#: compensation/forms/modals/state.py:91 konova/forms/modals/base_form.py:32
msgid "Object removed"
msgstr "Objekt entfernt"
#: compensation/forms/modals/state.py:145
#: compensation/forms/modals/state.py:146
#: compensation/templates/compensation/detail/compensation/includes/states-after.html:62
#: compensation/templates/compensation/detail/compensation/includes/states-before.html:62
#: compensation/templates/compensation/detail/eco_account/includes/states-after.html:62
@@ -1287,44 +1288,44 @@ msgstr ""
msgid "Responsible data"
msgstr "Daten zu den verantwortlichen Stellen"
#: compensation/views/compensation/compensation.py:57
#: compensation/views/compensation/compensation.py:58
msgid "Compensations - Overview"
msgstr "Kompensationen - Übersicht"
#: compensation/views/compensation/compensation.py:180
#: compensation/views/compensation/compensation.py:181
#: konova/utils/message_templates.py:40
msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet"
#: compensation/views/compensation/compensation.py:195
#: compensation/views/eco_account/eco_account.py:172 ema/views/ema.py:230
#: intervention/views/intervention.py:251
#: compensation/views/compensation/compensation.py:196
#: compensation/views/eco_account/eco_account.py:173 ema/views/ema.py:238
#: intervention/views/intervention.py:253
msgid "Edit {}"
msgstr "Bearbeite {}"
#: compensation/views/compensation/report.py:34
#: compensation/views/eco_account/report.py:34 ema/views/report.py:34
#: intervention/views/report.py:37
#: compensation/views/compensation/report.py:35
#: compensation/views/eco_account/report.py:36 ema/views/report.py:35
#: intervention/views/report.py:35
msgid "Report {}"
msgstr "Bericht {}"
#: compensation/views/eco_account/eco_account.py:52
#: compensation/views/eco_account/eco_account.py:53
msgid "Eco-account - Overview"
msgstr "Ökokonten - Übersicht"
#: compensation/views/eco_account/eco_account.py:85
#: compensation/views/eco_account/eco_account.py:86
msgid "Eco-Account {} added"
msgstr "Ökokonto {} hinzugefügt"
#: compensation/views/eco_account/eco_account.py:157
#: compensation/views/eco_account/eco_account.py:158
msgid "Eco-Account {} edited"
msgstr "Ökokonto {} bearbeitet"
#: compensation/views/eco_account/eco_account.py:286
#: compensation/views/eco_account/eco_account.py:288
msgid "Eco-account removed"
msgstr "Ökokonto entfernt"
#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:101
#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:108
msgid "New EMA"
msgstr "Neue EMA hinzufügen"
@@ -1352,19 +1353,19 @@ msgstr ""
msgid "Payment funded compensation"
msgstr "Ersatzzahlungsmaßnahme"
#: ema/views/ema.py:52
#: ema/views/ema.py:53
msgid "EMAs - Overview"
msgstr "EMAs - Übersicht"
#: ema/views/ema.py:85
#: ema/views/ema.py:86
msgid "EMA {} added"
msgstr "EMA {} hinzugefügt"
#: ema/views/ema.py:215
#: ema/views/ema.py:223
msgid "EMA {} edited"
msgstr "EMA {} bearbeitet"
#: ema/views/ema.py:254
#: ema/views/ema.py:262
msgid "EMA removed"
msgstr "EMA entfernt"
@@ -1428,7 +1429,7 @@ msgstr "Datum Bestandskraft bzw. Rechtskraft"
#: intervention/forms/intervention.py:216
#: intervention/tests/unit/test_forms.py:36
#: intervention/views/intervention.py:104
#: intervention/views/intervention.py:105
msgid "New intervention"
msgstr "Neuer Eingriff"
@@ -1597,7 +1598,12 @@ msgctxt "money"
msgid "Amount"
msgstr "Betrag"
#: intervention/templates/intervention/detail/includes/payments.html:62
#: intervention/templates/intervention/detail/includes/payments.html:61
#: konova/utils/message_templates.py:25
msgid "This data is not shared with you"
msgstr "Diese Daten sind für Sie nicht freigegeben"
#: intervention/templates/intervention/detail/includes/payments.html:70
msgid "Remove payment"
msgstr "Zahlung entfernen"
@@ -1659,19 +1665,19 @@ msgstr ""
msgid "Check performed"
msgstr "Prüfung durchgeführt"
#: intervention/views/intervention.py:56
#: intervention/views/intervention.py:57
msgid "Interventions - Overview"
msgstr "Eingriffe - Übersicht"
#: intervention/views/intervention.py:89
#: intervention/views/intervention.py:90
msgid "Intervention {} added"
msgstr "Eingriff {} hinzugefügt"
#: intervention/views/intervention.py:234
#: intervention/views/intervention.py:236
msgid "Intervention {} edited"
msgstr "Eingriff {} bearbeitet"
#: intervention/views/intervention.py:276
#: intervention/views/intervention.py:278
msgid "{} removed"
msgstr "{} entfernt"
@@ -1781,6 +1787,16 @@ msgstr ""
"Wenn aktiviert werden auch Einträge angezeigt, die nicht für Sie freigegeben "
"sind"
#: konova/filters/mixins/user_log.py:21
msgid "Logged user"
msgstr "Bearbeitender Nutzer"
#: konova/filters/mixins/user_log.py:22
msgid ""
"Search for entries where this person has been participated according to log "
"history"
msgstr "Sucht nach Einträgen, an denen diese Person gearbeitet hat"
#: konova/forms/base_form.py:23 templates/form/collapsable/form.html:62
msgid "Save"
msgstr "Speichern"
@@ -1799,10 +1815,6 @@ msgid "Only surfaces allowed. Points or lines must be buffered."
msgstr ""
"Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden."
#: konova/forms/geometry_form.py:153
msgid "Geometry must be greater than 1m². Currently is {}m²"
msgstr "Geometrie muss größer als 1m² sein. Aktueller Flächeninhalt: {}m²"
#: konova/forms/modals/document_form.py:37
msgid "When has this file been created? Important for photos."
msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?"
@@ -1841,6 +1853,7 @@ msgstr ""
"Ich, {} {}, bestätige, dass diese Daten wieder entzeichnet werden müssen."
#: konova/forms/modals/remove_form.py:22 konova/forms/remove_form.py:24
#: user/forms/modals/api_token.py:16
msgid "Confirm"
msgstr "Bestätige"
@@ -1911,11 +1924,11 @@ msgstr "Kontrolle am"
msgid "Other"
msgstr "Sonstige"
#: konova/sub_settings/django_settings.py:166
#: konova/sub_settings/django_settings.py:156
msgid "German"
msgstr ""
#: konova/sub_settings/django_settings.py:167
#: konova/sub_settings/django_settings.py:157
msgid "English"
msgstr ""
@@ -2091,10 +2104,6 @@ msgstr ""
"Eintrag ist verzeichnet. Um Daten zu bearbeiten, muss der Eintrag erst "
"entzeichnet werden."
#: konova/utils/message_templates.py:25
msgid "This data is not shared with you"
msgstr "Diese Daten sind für Sie nicht freigegeben"
#: konova/utils/message_templates.py:26
msgid ""
"Remember: This data has not been shared with you, yet. This means you can "
@@ -2253,34 +2262,35 @@ msgstr ""
"Die Geometrie enthielt mehr als {} Eckpunkte. Sie musste vereinfacht werden "
"um die Obergrenze von {} erlaubten Eckpunkten einzuhalten."
#: konova/utils/message_templates.py:88
#: konova/utils/message_templates.py:86
msgid ""
"The geometry contained {} parts which have been detected as invalid (e.g. "
"too small to be valid). These parts have been removed. Please check the "
"stored geometry."
msgstr ""
"Die Geometrie enthielt {} invalide Bestandteile (z.B. unaussagekräftige Kleinstflächen)."
"Diese Bestandteile wurden automatisch entfernt. Bitte überprüfen Sie die angepasste Geometrie."
#: konova/utils/message_templates.py:89
msgid "This intervention has {} revocations"
msgstr "Dem Eingriff liegen {} Widersprüche vor"
#: konova/utils/message_templates.py:91
#: konova/utils/message_templates.py:92
msgid "Checked on {} by {}"
msgstr "Am {} von {} geprüft worden"
#: konova/utils/message_templates.py:92
#: konova/utils/message_templates.py:93
msgid "Data has changed since last check on {} by {}"
msgstr ""
"Daten wurden nach der letzten Prüfung geändert. Letzte Prüfung am {} durch {}"
#: konova/utils/message_templates.py:93
#: konova/utils/message_templates.py:94
msgid "Current data not checked yet"
msgstr "Momentane Daten noch nicht geprüft"
#: konova/utils/messenger.py:70
msgid "{} checked"
msgstr "{} geprüft"
#: konova/utils/messenger.py:72
msgid "<a href=\"{}\">Check it out</a>"
msgstr "<a href=\"{}\">Schauen Sie rein</a>"
#: konova/utils/messenger.py:73
msgid "{} has been checked successfully by user {}! {}"
msgstr "{} wurde erfolgreich vom Nutzer {} geprüft! {}"
#: konova/utils/message_templates.py:97
msgid "New token generated. Administrators need to validate."
msgstr "Neuer Token generiert. Administratoren sind informiert."
#: konova/utils/quality.py:32
msgid "missing"
@@ -2308,14 +2318,6 @@ msgstr "Home"
msgid "Log"
msgstr "Log"
#: konova/views/map_proxy.py:70
msgid ""
"The external service is currently unavailable.<br>Please try again in a few "
"moments..."
msgstr ""
"Der externe Dienst ist zur Zeit nicht erreichbar.<br>Versuchen Sie es in ein "
"paar Sekunden nochmal."
#: konova/views/record.py:30
msgid "{} unrecorded"
msgstr "{} entzeichnet"
@@ -2372,6 +2374,20 @@ msgstr "Alle"
msgid "News"
msgstr "Neuigkeiten"
#: templates/400.html:12
msgid "Request was invalid"
msgstr "Anfrage fehlerhaft"
#: templates/400.html:17
msgid "There seems to be a problem with the link you opened."
msgstr "Es scheint ein Problem mit dem Link zu geben, den Sie geöffnet haben."
#: templates/400.html:18
msgid "Make sure the URL is valid (no whitespaces, properly copied, ...)."
msgstr ""
"Stellen Sie sicher, dass die URL gültig ist (keine Leerzeichen, fehlerfrei "
"kopiert, ...)."
#: templates/404.html:7
msgid "Not found"
msgstr "Nicht gefunden"
@@ -2384,11 +2400,11 @@ msgstr "Die angeforderten Daten existieren nicht."
msgid "Make sure the URL is valid (no whitespaces, ...)."
msgstr "Stellen Sie sicher, dass die URL gültig ist (keine Leerzeichen, ...)."
#: templates/500.html:7
#: templates/500.html:12
msgid "Server Error"
msgstr ""
#: templates/500.html:10
#: templates/500.html:17
msgid "Something happened. Admins have been informed. We are working on it!"
msgstr ""
"Irgendetwas ist passiert. Die Administratoren wurden informiert. Wir "
@@ -2817,11 +2833,15 @@ msgstr "Mehr"
msgid "Reports"
msgstr "Berichte"
#: templates/navbars/navbar.html:56 user/templates/user/index.html:31
#: templates/navbars/navbar.html:57
msgid "Admin"
msgstr ""
#: templates/navbars/navbar.html:59 user/templates/user/index.html:31
msgid "Settings"
msgstr "Einstellungen"
#: templates/navbars/navbar.html:57
#: templates/navbars/navbar.html:60
msgid "Logout"
msgstr "Abmelden"
@@ -2854,6 +2874,22 @@ msgstr ""
"Falls die Geometrie nicht leer ist, werden die Flurstücke aktuell berechnet. "
"Bitte laden Sie diese Seite in ein paar Augenblicken erneut..."
#: user/forms/modals/api_token.py:25
msgid "Generate API Token"
msgstr "API Token generieren"
#: user/forms/modals/api_token.py:29
msgid ""
"You are about to create a new API token. The existing one will not be usable "
"afterwards."
msgstr ""
"Wenn Sie fortfahren, generieren Sie einen neuen API Token. Ihren "
"existierenden werden Sie dann nicht länger nutzen können."
#: user/forms/modals/api_token.py:31
msgid "A new token needs to be validated by an administrator!"
msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!"
#: user/forms/modals/team.py:20 user/forms/modals/team.py:24
#: user/forms/team.py:17 user/forms/team.py:22
msgid "Team name"
@@ -2876,11 +2912,11 @@ msgstr ""
"Mehrfachauswahl möglich - Sie können nur Nutzer wählen, die noch nicht "
"Mitglieder dieses Teams sind. Geben Sie den ganzen Nutzernamen an."
#: user/forms/modals/team.py:56 user/tests/unit/test_forms.py:31
#: user/forms/modals/team.py:56 user/tests/unit/test_forms.py:30
msgid "Create new team"
msgstr "Neues Team anlegen"
#: user/forms/modals/team.py:57 user/tests/unit/test_forms.py:32
#: user/forms/modals/team.py:57 user/tests/unit/test_forms.py:31
msgid ""
"You will become the administrator for this group by default. You do not need "
"to add yourself to the list of members."
@@ -2909,11 +2945,11 @@ msgid "There must be at least one admin on this team."
msgstr "Es muss mindestens einen Administrator für das Team geben."
#: user/forms/modals/team.py:160 user/templates/user/team/index.html:60
#: user/tests/unit/test_forms.py:88
#: user/tests/unit/test_forms.py:87
msgid "Edit team"
msgstr "Team bearbeiten"
#: user/forms/modals/team.py:187 user/tests/unit/test_forms.py:165
#: user/forms/modals/team.py:187 user/tests/unit/test_forms.py:164
msgid ""
"ATTENTION!\n"
"\n"
@@ -2930,7 +2966,7 @@ msgstr ""
"Sind Sie sicher, dass Sie dieses Team löschen möchten?"
#: user/forms/modals/team.py:197 user/templates/user/team/index.html:56
#: user/tests/unit/test_forms.py:198
#: user/tests/unit/test_forms.py:197
msgid "Leave team"
msgstr "Team verlassen"
@@ -2962,22 +2998,10 @@ msgstr "Benachrichtigungen"
msgid "Select the situations when you want to receive a notification"
msgstr "Wann wollen Sie per E-Mail benachrichtigt werden?"
#: user/forms/user.py:38 user/tests/unit/test_forms.py:234
#: user/forms/user.py:38 user/tests/unit/test_forms.py:233
msgid "Edit notifications"
msgstr "Benachrichtigungen bearbeiten"
#: user/forms/user.py:73
msgid "Token"
msgstr ""
#: user/forms/user.py:88 user/tests/unit/test_forms.py:260
msgid "Create new token"
msgstr "Neuen Token generieren"
#: user/forms/user.py:89 user/tests/unit/test_forms.py:261
msgid "A new token needs to be validated by an administrator!"
msgstr "Neue Tokens müssen durch Administratoren freigeschaltet werden!"
#: user/models/user_action.py:23
msgid "Unrecorded"
msgstr "Entzeichnet"
@@ -3032,7 +3056,7 @@ msgid "Manage teams"
msgstr ""
#: user/templates/user/index.html:53 user/templates/user/team/index.html:19
#: user/views.py:171
#: user/views/views.py:135
msgid "Teams"
msgstr ""
@@ -3068,270 +3092,58 @@ msgstr "API Einstellungen"
msgid "Current token"
msgstr "Aktueller Token"
#: user/templates/user/token.html:14
#: user/templates/user/token.html:15
msgid "Create new token"
msgstr "Neuen Token generieren"
#: user/templates/user/token.html:23
msgid "Authenticated by admins"
msgstr "Von Admin freigeschaltet"
#: user/templates/user/token.html:18
#: user/templates/user/token.html:27
msgid "Token has been verified and can be used"
msgstr "Token wurde freigeschaltet und kann verwendet werden"
#: user/templates/user/token.html:20
#: user/templates/user/token.html:29
msgid "Token waiting for verification"
msgstr "Token noch nicht freigeschaltet"
#: user/templates/user/token.html:24
#: user/templates/user/token.html:33
msgid "Valid until"
msgstr "Läuft ab am"
#: user/views.py:35
msgid "User settings"
msgstr "Einstellungen"
#: user/views.py:61
msgid "Notifications edited"
msgstr "Benachrichtigungen bearbeitet"
#: user/views.py:73
msgid "User notifications"
msgstr "Benachrichtigungen"
#: user/views.py:96
msgid "New token generated. Administrators need to validate."
msgstr "Neuer Token generiert. Administratoren sind informiert."
#: user/views.py:107
#: user/views/api_token.py:34
msgid "User API token"
msgstr "API Nutzer Token"
#: user/views.py:183
#: user/views/views.py:33
msgid "User settings"
msgstr "Einstellungen"
#: user/views/views.py:59
msgid "Notifications edited"
msgstr "Benachrichtigungen bearbeitet"
#: user/views/views.py:71
msgid "User notifications"
msgstr "Benachrichtigungen"
#: user/views/views.py:147
msgid "New team added"
msgstr "Neues Team hinzugefügt"
#: user/views.py:198
#: user/views/views.py:162
msgid "Team edited"
msgstr "Team bearbeitet"
#: user/views.py:213
#: user/views/views.py:177
msgid "Team removed"
msgstr "Team gelöscht"
#: user/views.py:228
#: user/views/views.py:192
msgid "You are not a member of this team"
msgstr "Sie sind kein Mitglied dieses Teams"
#: user/views.py:235
#: user/views/views.py:199
msgid "Left Team"
msgstr "Team verlassen"
#~ msgid "close"
#~ msgstr "Schließen"
#~ msgid "Options"
#~ msgstr "Optionen"
#~ msgid "Commands"
#~ msgstr "Befehle"
#~ msgid "Missing command."
#~ msgstr "Befehl fehlt"
#~ msgid "Missing argument"
#~ msgstr "Argument fehlt"
#~ msgid "Missing option"
#~ msgstr "Option fehlt"
#~ msgid "Missing parameter"
#~ msgstr "Parameter fehlt"
#~ msgid "Messages"
#~ msgstr "Nachrichten"
#~ msgid "This field is required."
#~ msgstr "Pflichtfeld"
#~ msgid "Monday"
#~ msgstr "Montag"
#~ msgid "Tuesday"
#~ msgstr "Dienstag"
#~ msgid "Wednesday"
#~ msgstr "Mittwoch"
#~ msgid "Thursday"
#~ msgstr "Donnerstag"
#~ msgid "Friday"
#~ msgstr "Freitag"
#~ msgid "Saturday"
#~ msgstr "Samstag"
#~ msgid "Sunday"
#~ msgstr "Sonntag"
#~ msgid "Mon"
#~ msgstr "Mo"
#~ msgid "Tue"
#~ msgstr "Di"
#~ msgid "Wed"
#~ msgstr "Mi"
#~ msgid "Thu"
#~ msgstr "Do"
#~ msgid "Fri"
#~ msgstr "Fr"
#~ msgid "Sat"
#~ msgstr "Sa"
#~ msgid "Sun"
#~ msgstr "So"
#~ msgid "January"
#~ msgstr "Januar"
#~ msgid "February"
#~ msgstr "Februar"
#~ msgid "March"
#~ msgstr "März"
#~ msgid "May"
#~ msgstr "Mai"
#~ msgid "June"
#~ msgstr "Juni"
#~ msgid "July"
#~ msgstr "Juli"
#~ msgid "October"
#~ msgstr "Oktober"
#~ msgid "December"
#~ msgstr "Dezember"
#~ msgid "mar"
#~ msgstr "mär"
#~ msgid "may"
#~ msgstr "mai"
#~ msgid "oct"
#~ msgstr "okt"
#~ msgid "dec"
#~ msgstr "dez"
#~ msgctxt "abbrev. month"
#~ msgid "March"
#~ msgstr "Mär"
#~ msgctxt "abbrev. month"
#~ msgid "May"
#~ msgstr "Mai"
#~ msgctxt "abbrev. month"
#~ msgid "June"
#~ msgstr "Juni"
#~ msgctxt "abbrev. month"
#~ msgid "July"
#~ msgstr "Juli"
#~ msgctxt "abbrev. month"
#~ msgid "Oct."
#~ msgstr "Okt."
#~ msgctxt "abbrev. month"
#~ msgid "Dec."
#~ msgstr "Dez."
#~ msgctxt "alt. month"
#~ msgid "January"
#~ msgstr "Januar"
#~ msgctxt "alt. month"
#~ msgid "February"
#~ msgstr "Februar"
#~ msgctxt "alt. month"
#~ msgid "March"
#~ msgstr "März"
#~ msgctxt "alt. month"
#~ msgid "May"
#~ msgstr "Mai"
#~ msgctxt "alt. month"
#~ msgid "June"
#~ msgstr "Juni"
#~ msgctxt "alt. month"
#~ msgid "July"
#~ msgstr "Juli"
#~ msgctxt "alt. month"
#~ msgid "October"
#~ msgstr "Oktober"
#~ msgctxt "alt. month"
#~ msgid "December"
#~ msgstr "Dezember"
#~ msgid "or"
#~ msgstr "oder"
#~ msgid ""
#~ "Deductable surface can not be larger than existing surfaces in after "
#~ "states"
#~ msgstr ""
#~ "Die abbuchbare Fläche darf die Gesamtfläche der Zielzustände nicht "
#~ "überschreiten"
#~ msgid ""
#~ "Deductable surface can not be smaller than the sum of already existing "
#~ "deductions. Please contact the responsible users for the deductions!"
#~ msgstr ""
#~ "Es wurde bereits mehr Fläche abgebucht, als Sie nun als abbuchbar "
#~ "einstellen wollen. Kontaktieren Sie die für die Abbuchungen "
#~ "verantwortlichen Nutzer!"
#~ msgid "Added deadline"
#~ msgstr "Frist/Termin hinzugefügt"
#~ msgid "Change default configuration for your KSP map"
#~ msgstr "Karteneinstellungen ändern"
#~ msgid "Map settings"
#~ msgstr "Karte"
#~ msgid "There are errors on this intervention:"
#~ msgstr "Es liegen Fehler in diesem Eingriff vor:"
#~ msgid "Before"
#~ msgstr "Vor"
#~ msgid "Groups"
#~ msgstr "Gruppen"
#~ msgid "Show more..."
#~ msgstr "Mehr anzeigen..."
#~ msgid "Kreis"
#~ msgstr "Kreis"
#~ msgid "Gemarkung"
#~ msgstr "Gemarkung"
#~ msgid "Loading..."
#~ msgstr "Lade..."
#~ msgid "Who handles the eco-account"
#~ msgstr "Wer für die Herrichtung des Ökokontos verantwortlich ist"

25
nginx.conf Normal file
View File

@@ -0,0 +1,25 @@
server {
listen 80;
client_max_body_size 25M;
location / {
proxy_pass http://127.0.0.1:8000;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Host $host;
proxy_redirect off;
proxy_cache_bypass $http_upgrade;
}
location /static/ {
alias /konova/static/;
access_log /var/log/nginx/access.log;
autoindex off;
types {
text/css css;
application/javascript js;
}
}
error_log /var/log/nginx/error.log;
}

View File

@@ -1,65 +1,63 @@
amqp==5.2.0
amqp==5.3.1
asgiref==3.8.1
async-timeout==4.0.3
async-timeout==5.0.1
beautifulsoup4==4.13.0b2
billiard==4.2.0
cached-property==1.5.2
billiard==4.2.1
cached-property==2.0.1
celery==5.4.0
certifi==2024.7.4
cffi==1.17.0rc1
certifi==2024.12.14
cffi==1.17.1
chardet==5.2.0
charset-normalizer==3.3.2
click==8.1.7
charset-normalizer==3.4.0
click==8.1.8
click-didyoumean==0.3.1
click-plugins==1.1.1
click-repl==0.3.0
coverage==7.5.4
cryptography==42.0.8
Deprecated==1.2.14
Django==5.0.7
coverage==7.6.9
cryptography==44.0.0
Deprecated==1.2.15
Django==5.1.4
django-autocomplete-light==3.11.0
django-bootstrap-modal-forms==3.0.4
django-bootstrap4==24.3
django-bootstrap-modal-forms==3.0.5
django-bootstrap4==24.4
django-environ==0.11.2
django-filter==24.2
django-filter==24.3
django-fontawesome-5==1.0.18
django-oauth-toolkit==2.4.0
django-simple-sso==1.2.0
django-tables2==2.7.0
et-xmlfile==1.1.0
gunicorn==22.0.0
idna==3.7
importlib_metadata==8.0.0
itsdangerous==0.24
django-oauth-toolkit==3.0.1
django-tables2==2.7.1
et_xmlfile==2.0.0
gunicorn==23.0.0
idna==3.10
importlib_metadata==8.5.0
jwcrypto==1.5.6
kombu==5.4.0rc1
oauthlib==3.2.2
openpyxl==3.2.0b1
packaging==24.1
packaging==24.2
pika==1.3.2
pillow==10.4.0
prompt_toolkit==3.0.47
psycopg==3.2.1
psycopg-binary==3.2.1
pillow==11.0.0
prompt_toolkit==3.0.48
psycopg==3.2.3
psycopg-binary==3.2.3
pycparser==2.22
pyparsing==3.1.2
pyparsing==3.2.0
pypng==0.20220715.0
pyproj==3.6.1
pyproj==3.7.0
python-dateutil==2.9.0.post0
pytz==2024.1
PyYAML==6.0.2rc1
pytz==2024.2
PyYAML==6.0.2
qrcode==7.3.1
redis==5.1.0b6
requests<2.32.0
six==1.16.0
soupsieve==2.5
sqlparse==0.5.0
sqlparse==0.5.1
typing_extensions==4.12.2
tzdata==2024.1
urllib3==2.2.2
tzdata==2024.2
urllib3==2.3.0
vine==5.1.0
wcwidth==0.2.13
webservices==0.7
wrapt==1.16.0
xmltodict==0.13.0
zipp==3.19.2
xmltodict==0.14.2
zipp==3.21.0

21
templates/400.html Normal file
View File

@@ -0,0 +1,21 @@
{% extends 'public_base.html' %}
{% load i18n fontawesome_5 static %}
{% block body %}
<div class="jumbotron">
<div class="row">
<div class="col-auto">
<img src="{% static 'images/error_imgs/croc_technician_400.png' %}" style="max-width: 150px">
</div>
<div class="col-sm-12 col-md-9 col-lg-9 col-xl-10">
<h1 class="display-4">{% fa5_icon 'question-circle' %}400</h1>
<h1 class="display-4">{% trans 'Request was invalid' %}</h1>
</div>
</div>
<hr>
<p class="lead">
{% trans 'There seems to be a problem with the link you opened.' %}
{% trans 'Make sure the URL is valid (no whitespaces, properly copied, ...).' %}
</p>
</div>
{% endblock %}

View File

@@ -1,10 +1,17 @@
{% extends 'public_base.html' %}
{% load i18n fontawesome_5 %}
{% load i18n fontawesome_5 static %}
{% block body %}
<div class="jumbotron">
<h1 class="display-4">{% fa5_icon 'fire-extinguisher' %} {% fa5_icon 'fire-alt' %} 500</h1>
<h1 class="display-4">{% trans 'Server Error' %}</h1>
<div class="row">
<div class="col-auto">
<img src="{% static 'images/error_imgs/croc_technician_500.png' %}" style="max-width: 150px">
</div>
<div class="col-sm-12 col-md-9 col-lg-9 col-xl-10">
<h1 class="display-4">{% fa5_icon 'fire-alt' %} 500</h1>
<h1 class="display-4">{% trans 'Server Error' %}</h1>
</div>
</div>
<hr>
<p class="lead">
{% trans 'Something happened. Admins have been informed. We are working on it!' %}

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