Compare commits

...

190 Commits

Author SHA1 Message Date
mpeltriaux 59fca60134 Merge pull request 'Master' (#587) from master into Docker
Reviewed-on: #587
2026-09-13 12:13:36 +02:00
mpeltriaux b6b740a31c Merge pull request '585 invalid 25832 coordinates' (#586) from 585_Invalid_25832_coordinates into master
Reviewed-on: #586
2026-09-13 12:12:57 +02:00
mpeltriaux 7c200d9034 # Rename check method
* renames is_valid_coord_area to is_valid_coord_range for more clarity
2026-09-13 12:11:18 +02:00
mpeltriaux 750715105e # EPSG:25832 Coordinate validity
* adds epsg:25832 coordinate range check to GeoJsonValidator
* improves resolving of min/max coordinates from envelope on is_valid_coord_area()
2026-09-13 12:10:37 +02:00
mpeltriaux a994554650 # Coordinate validity check
* refactors coordinate validity check in generic base method
* introduces specific check for epsg:25832 (RLP)
* refactors check for epsg:4326 to use generic base method
2026-09-13 11:51:10 +02:00
mpeltriaux 4c7c667bdb # Error fetching
* adds proper fetching of GDAL Exceptions in case of further problems in the future
2026-09-13 11:36:28 +02:00
mpeltriaux 7e136f4022 Merge pull request '# HOTFIX' (#584) from hotfix_feature_checl into master
Reviewed-on: #584
2026-09-12 18:30:56 +02:00
mpeltriaux c8a39e62eb Merge branch 'hotfix_feature_checl' into Docker 2026-09-12 18:30:48 +02:00
mpeltriaux 1393491fb7 # HOTFIX
* fixes bug where specific case leads to no-feature detection
2026-09-12 18:30:13 +02:00
mpeltriaux b1ca199b85 # Bullseye to Trixie
* updates base image from debian 11 to 13
2026-09-12 18:22:16 +02:00
mpeltriaux 3354ad1f68 Merge pull request 'Master' (#583) from master into Docker
Reviewed-on: #583
2026-09-12 18:00:09 +02:00
mpeltriaux a2a726fba2 Merge pull request '565 unify geometry quality checking' (#582) from 565_Unify_geometry_quality_checking into master
Reviewed-on: #582
2026-09-12 17:58:55 +02:00
mpeltriaux 8ac8952729 # Refactors API geometry processing
* refactors API geometry processing to use GeoJsonValidator and GeometryProcessor just like SimpleGeomForm
* moves cast_to_multipolygon, cast_to_rlp_srid, flatten_geom_to_2D, is_valid_4326 and is_area_valid into GeometryProcessor
* adds changeable SRID to GeoJsonValidator constructor
* updates test data
* updates test cases
2026-09-12 17:58:00 +02:00
mpeltriaux a067aafde1 # Refactoring geometry processing
* adds GeometryProcessor
* adds GeoJsonValidator
* refactors SimpleGeomForm is_valid() logic using these new classes
2026-09-12 16:34:34 +02:00
mpeltriaux 84b4acfb00 Merge pull request '580 uuid resolving 4xx error' (#581) from 580_UUID_resolving_4xx_error into master
Reviewed-on: #581
2026-09-12 15:40:29 +02:00
mpeltriaux 552811caf3 # Requirements update
* updates requirements.txt to latest stable versions
2026-09-12 15:39:35 +02:00
mpeltriaux 80ec64c002 # Client error code
* refactors internal server error (validationError) into assertionError (client error)
2026-09-12 15:29:47 +02:00
mpeltriaux 960b681ab9 Merge pull request 'Master' (#579) from master into Docker
Reviewed-on: #579
2026-08-20 08:03:07 +02:00
mpeltriaux 092e30687a Merge pull request '# Fix' (#578) from fix_mass_delete_command into master
Reviewed-on: #578
2026-08-20 08:02:35 +02:00
mpeltriaux 0eb7cb8c0b # Fix
* comments trigger line back in
2026-08-20 08:01:56 +02:00
mpeltriaux e99b371d5d Merge pull request 'Master' (#577) from master into Docker
Reviewed-on: #577
2026-08-20 07:52:54 +02:00
mpeltriaux 58d3c54920 Merge pull request '# Mass delete script' (#576) from 573_Script_mass_delete into master
Reviewed-on: #576
2026-08-17 15:20:46 +02:00
mpeltriaux dc16efcc64 # Mass delete script
* extends mass delete command so that not only entries can be marked as deleted but also finally be deleted from the database
2026-08-17 15:19:42 +02:00
mpeltriaux 906d6335ff Merge pull request '# Remove ExternalIdentifier' (#575) from 574_Remove_externalIdentifier into master
Reviewed-on: #575
2026-08-17 14:37:00 +02:00
mpeltriaux 6772ddb5c8 # Remove ExternalIdentifier
* adds removing of ExternalIdentifier entries if a BaseObject is removed from the database
* removes unused import statements
* adds ExternalIdentifier view to admin backend
2026-08-17 14:35:42 +02:00
mpeltriaux ba7f5edc33 Merge pull request 'Master' (#571) from master into Docker
Reviewed-on: #571
2026-08-04 06:54:59 +02:00
mpeltriaux ea1276e40b Merge pull request '# HOTFIX OEK report' (#570) from fix_oek_report_empty_deduction_list into master
Reviewed-on: #570
2026-08-04 06:54:42 +02:00
mpeltriaux ee4701f63f # HOTFIX OEK report
* fixes wrong table layout on empty deduction list of a recorded oek
2026-08-04 06:53:29 +02:00
mpeltriaux 3aa7b4874e Merge pull request 'Update docker' (#569) from master into Docker
Reviewed-on: #569
2026-08-03 13:08:32 +02:00
mpeltriaux 87e4ee726c Merge pull request '# Extended OEK report' (#568) from 564_Show_deductions_on_EcoAccount_report into master
Reviewed-on: #568
2026-08-03 13:07:34 +02:00
mpeltriaux cbe309f114 # Extended OEK report
* adds available rest surface of eco account to public report
* adds amount of deducted surface per each deduction to public report
2026-08-03 13:06:51 +02:00
mpeltriaux 8ade4c2f7a Merge pull request 'Master' (#567) from master into Docker
Reviewed-on: #567
2026-07-16 15:43:33 +02:00
mpeltriaux 2aa625a6ac Merge pull request '# Quickfix' (#566) from 565_Unify_geometry_quality_checking into master
Reviewed-on: #566
2026-07-16 15:43:06 +02:00
mpeltriaux d39e758df4 # Quickfix
* fixes bug for better API response on geometry invalidities
2026-07-16 15:42:29 +02:00
mpeltriaux 67be62a869 Merge pull request 'Master' (#563) from master into Docker
Reviewed-on: #563
2026-07-16 10:39:18 +02:00
mpeltriaux 690d11e966 Merge pull request '# Mass delete command' (#562) from 561_Mass_delete_command_via_identifier into master
Reviewed-on: #562
2026-07-16 10:38:43 +02:00
mpeltriaux 02ce78551c # Mass delete command
* introduces new command for mass deleting entries based on specific pattern in identifier
    * command guides user through parameter collecting process
2026-07-16 10:38:00 +02:00
mpeltriaux 5a8d20047f Merge pull request 'Master' (#560) from master into Docker
Reviewed-on: #560
2026-07-10 19:03:36 +02:00
mpeltriaux 112b74f5cd Merge pull request '# Hotfix' (#559) from hotfix_API_error_msg_on_wrong_spatial_ref into master
Reviewed-on: #559
2026-07-10 19:02:51 +02:00
mpeltriaux 22a92cb3df # Hotfix
* adds special error message treatment for non-EPSG:4326 geometries
2026-07-10 19:01:24 +02:00
mpeltriaux 74d4ee49cc Merge pull request 'Master' (#558) from master into Docker
Reviewed-on: #558
2026-07-10 14:03:05 +02:00
mpeltriaux f5aaac2acf Merge pull request '# Basemap' (#557) from 546_Remove_WebAtlasRP_service into master
Reviewed-on: #557
2026-07-10 14:01:38 +02:00
mpeltriaux 775e40e9ae # Basemap
* removes discontinued webatlas wms layers from config.json
* enables basemap_grau as default wms layer
2026-07-10 14:00:36 +02:00
mpeltriaux e794f3fff2 Merge pull request '# Improvement PUT API' (#556) from 554_API_PUT_external_identifier into master
Reviewed-on: #556
2026-07-10 13:59:22 +02:00
mpeltriaux b4f2e3232a # Improvement PUT API
* improves error processing and response on PUT endpoint
* consolidates improved error processing into central method for all API endpoints
2026-07-10 13:58:23 +02:00
mpeltriaux e8b583d4c6 Merge pull request 'Master' (#553) from master into Docker
Reviewed-on: #553
2026-06-27 09:32:26 +02:00
mpeltriaux b9c453fdd2 Merge pull request '549 rework geometry conflict fetching' (#552) from 549_Rework_GeometryConflict_fetching into master
Reviewed-on: #552
2026-06-27 09:31:08 +02:00
mpeltriaux 625c591122 # Tests
* updates tests
* enhances workflow of geometry conflict fetching
2026-06-25 08:55:50 +02:00
mpeltriaux f551763798 # Performance boost
* boosts performance of geometry conflict fetching 50-75%
2026-06-25 08:27:09 +02:00
mpeltriaux f57fa4cc20 Merge pull request 'Master' (#551) from master into Docker
Reviewed-on: #551
2026-06-20 12:11:18 +02:00
mpeltriaux b0f72584e2 Merge pull request 'Master' (#548) from master into Docker
Reviewed-on: #548
2026-06-20 09:28:12 +02:00
mpeltriaux 2bbc780c0b Merge pull request 'Master' (#545) from master into Docker
Reviewed-on: #545
2026-06-17 11:59:35 +02:00
mpeltriaux 15a1bbe771 Merge pull request 'Master' (#543) from master into Docker
Reviewed-on: #543
2026-06-13 13:46:31 +02:00
mpeltriaux 9965ce2d2c Merge pull request 'master' (#540) from master into Docker
Reviewed-on: #540
2026-05-14 13:05:38 +00:00
mpeltriaux 89f309abd2 Merge pull request 'master' (#537) from master into Docker
Reviewed-on: #537
2026-05-10 10:15:13 +00:00
mpeltriaux f134948685 Merge branch 'master' into Docker 2026-03-01 14:31:45 +01:00
mpeltriaux 1e525b90a9 # Dockerfile upgrade
* upgrade baseimage to python 3.13
2026-03-01 14:04:58 +01:00
mpeltriaux 63a516e7d6 Merge branch 'master' into Docker
# Conflicts:
#	requirements.txt
2026-03-01 14:04:21 +01:00
mpeltriaux 2399ca9bf9 Merge pull request 'master' (#530) from master into Docker
Reviewed-on: #530
2026-01-30 08:44:00 +00:00
mpeltriaux c48ccd5334 Merge pull request 'master' (#528) from master into Docker
Reviewed-on: #528
2026-01-21 14:53:47 +00:00
mpeltriaux e2f8fc9c6d # Fix docker warnings
* fixes docker warnings about use of deprecated syntax
2026-01-21 15:52:07 +01:00
mpeltriaux fac658e52c Merge pull request 'master' (#524) from master into Docker
Reviewed-on: #524
2026-01-14 08:08:36 +00:00
mpeltriaux d6a65dd59a Merge pull request 'master' (#521) from master into Docker
Reviewed-on: #521
2026-01-13 09:37:16 +00:00
mpeltriaux cbc8acf6f6 Merge pull request 'master' (#519) from master into Docker
Reviewed-on: #519
2026-01-10 10:04:14 +00:00
mpeltriaux 31de477f26 Merge pull request 'master' (#516) from master into Docker
Reviewed-on: #516
2025-12-19 14:18:12 +01:00
mpeltriaux 19b6e633df Merge pull request 'master' (#514) from master into Docker
Reviewed-on: #514
2025-12-17 14:04:03 +01:00
mpeltriaux b98f821c98 Merge pull request 'master' (#511) from master into Docker
Reviewed-on: #511
2025-12-03 13:50:13 +01:00
mpeltriaux 5421de4e80 Merge pull request 'master' (#509) from master into Docker
Reviewed-on: #509
2025-11-30 12:33:58 +01:00
mpeltriaux a7a0044fc5 Merge pull request 'master' (#506) from master into Docker
Reviewed-on: #506
2025-11-28 11:45:48 +01:00
mpeltriaux 36552b3886 Merge pull request 'master' (#502) from master into Docker
Reviewed-on: #502
2025-11-19 13:16:48 +01:00
mpeltriaux a766c4dbe8 Merge pull request 'master' (#499) from master into Docker
Reviewed-on: #499
2025-11-07 14:12:18 +01:00
mpeltriaux 8126781b77 Merge pull request 'master' (#496) from master into Docker
Reviewed-on: #496
2025-10-23 16:13:04 +02:00
mpeltriaux a6a66d7499 Merge pull request 'master' (#493) from master into Docker
Reviewed-on: #493
2025-10-21 15:59:53 +02:00
mpeltriaux 1c0b67693d Merge pull request 'master' (#489) from master into Docker
Reviewed-on: #489
2025-10-15 09:51:46 +02:00
mpeltriaux ce6bb6b23b Merge pull request 'master' (#486) from master into Docker
Reviewed-on: #486
2025-10-12 11:32:27 +02:00
mpeltriaux 0b8176db2e Merge pull request 'master' (#484) from master into Docker
Reviewed-on: #484
2025-09-22 12:37:34 +02:00
mpeltriaux 3a299a040a Merge pull request 'master' (#482) from master into Docker
Reviewed-on: #482
2025-08-18 08:47:01 +02:00
mpeltriaux 3c5206139b Merge pull request 'master' (#477) from master into Docker
Reviewed-on: #477
2025-05-12 15:40:21 +02:00
mpeltriaux 6c53f39a28 Merge pull request 'master' (#474) from master into Docker
Reviewed-on: #474
2025-03-28 16:41:31 +01:00
mpeltriaux 64d8f47174 Merge pull request 'Docker_enhanced' (#472) from Docker_enhanced into Docker
Reviewed-on: #472
2025-03-28 16:11:18 +01:00
mpeltriaux f5f3246e89 # Docker enhancements
* optimizes nginx.conf
   * better logging of proxied requests
2025-03-24 14:17:08 +01:00
mpeltriaux 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
mpeltriaux c2c8630c82 Merge pull request 'master' (#471) from master into Docker
Reviewed-on: #471
2025-02-14 15:33:19 +01:00
mpeltriaux 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
mpeltriaux 2b84bab1d0 Merge pull request 'master' (#469) from master into Docker
Reviewed-on: #469
2025-01-24 16:12:33 +01:00
mpeltriaux 303583daa1 Merge pull request 'master' (#466) from master into Docker
Reviewed-on: #466
2025-01-21 13:44:14 +01:00
mpeltriaux d07b2ffbfb Merge pull request 'master' (#463) from master into Docker
Reviewed-on: #463
2025-01-08 16:05:15 +01:00
mpeltriaux 335800c44b Merge pull request 'master' (#459) from master into Docker
Reviewed-on: #459
2024-12-23 13:42:36 +01:00
mpeltriaux 5766cfde47 Merge pull request 'master' (#454) from master into Docker
Reviewed-on: #454
2024-12-23 12:09:47 +01:00
mpeltriaux 2ed3fcc0f9 Merge pull request 'master' (#449) from master into Docker
Reviewed-on: #449
2024-11-13 16:09:48 +01:00
mpeltriaux bf72295615 Merge pull request 'master' (#447) from master into Docker
Reviewed-on: #447
2024-10-26 10:25:06 +02:00
mpeltriaux 6b860f8ea5 Merge pull request 'master' (#445) from master into Docker
Reviewed-on: #445
2024-10-26 09:48:50 +02:00
mpeltriaux 2fa2fa547b Merge pull request 'master' (#443) from master into Docker
Reviewed-on: #443
2024-10-25 19:27:23 +02:00
mpeltriaux 3de956872c Merge pull request 'master' (#441) from master into Docker
Reviewed-on: #441
2024-10-25 14:24:55 +02:00
mpeltriaux 1c8e3992d6 Merge pull request 'master' (#438) from master into Docker
Reviewed-on: #438
2024-08-26 18:57:35 +02:00
mpeltriaux e6e9e141c8 Merge pull request 'master' (#436) from master into Docker
Reviewed-on: #436
2024-08-19 18:35:17 +02:00
mpeltriaux f8ece06ee8 Merge pull request 'master' (#431) from master into Docker
Reviewed-on: #431
2024-08-07 12:07:22 +02:00
mpeltriaux 149a351bfd Merge pull request 'master' (#429) from master into Docker
Reviewed-on: #429
2024-08-07 12:02:21 +02:00
mpeltriaux 0164717b8e Merge pull request 'master' (#426) from master into Docker
Reviewed-on: #426
2024-08-06 14:28:41 +02:00
mpeltriaux 104952bfc3 Merge pull request 'master' (#423) from master into Docker
Reviewed-on: #423
2024-07-10 09:30:30 +02:00
mpeltriaux f96241c8d1 Merge pull request 'master' (#421) from master into Docker
Reviewed-on: #421
2024-07-10 09:27:09 +02:00
mpeltriaux ac6b534f58 Merge pull request 'master' (#418) from master into Docker
Reviewed-on: #418
2024-07-08 18:44:22 +02:00
mpeltriaux 06910cd69a # Image tag
* increases image tag
2024-07-05 10:56:10 +02:00
mpeltriaux a48ba520fc Merge branch 'refs/heads/master' into Docker
# Conflicts:
#	konova/celery.py
2024-07-05 10:52:13 +02:00
mpeltriaux 9f18aa5890 Merge pull request 'master' (#414) from master into Docker
Reviewed-on: #414
2024-07-04 11:42:20 +02:00
mpeltriaux 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
mpeltriaux 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
mpeltriaux 0f2bf95b71 Merge pull request 'master' (#409) from master into Docker
Reviewed-on: #409
2024-06-18 11:50:43 +02:00
mpeltriaux 6a307016ec Merge pull request 'master' (#403) from master into Docker
Reviewed-on: #403
2024-05-17 10:59:17 +02:00
mpeltriaux 51017ef8fa Merge pull request 'master' (#401) from master into Docker
Reviewed-on: #401
2024-05-17 07:54:16 +02:00
mpeltriaux 05560534bc Merge pull request 'master' (#399) from master into Docker
Reviewed-on: #399
2024-05-16 17:37:56 +02:00
mpeltriaux 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
mpeltriaux 1d94211428 # Requirements update
* fixes requirements.txt due to django-simple-sso dependency
2024-04-12 08:51:18 +02:00
mpeltriaux 37357080d8 # Gunicorn update
* updates gunicorn package
2024-04-12 08:08:27 +02:00
mpeltriaux 5afa13ac92 Merge branch 'refs/heads/master' into Docker
# Conflicts:
#	requirements.txt
2024-04-12 08:07:05 +02:00
mpeltriaux 416cad1c8f Merge pull request 'master' (#392) from master into Docker
Reviewed-on: SGD-Nord/konova#392
2024-04-02 08:11:08 +02:00
mpeltriaux b5f83b7163 Merge pull request '# Requirements' (#390) from master into Docker
Reviewed-on: SGD-Nord/konova#390
2024-03-11 08:22:35 +01:00
mpeltriaux 20cfb5f345 Merge pull request 'master' (#389) from master into Docker
Reviewed-on: SGD-Nord/konova#389
2024-02-29 18:39:41 +01:00
mpeltriaux 88c96b95f2 Merge pull request '# HOTFIX' (#388) from master into Docker
Reviewed-on: SGD-Nord/konova#388
2024-02-21 18:32:16 +01:00
mpeltriaux f6c500b02a Merge pull request 'master' (#387) from master into Docker
Reviewed-on: SGD-Nord/konova#387
2024-02-16 10:16:43 +01:00
mpeltriaux d702cd8716 Merge pull request 'master' (#385) from master into Docker
Reviewed-on: SGD-Nord/konova#385
2024-02-16 08:45:32 +01:00
mpeltriaux 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
mpeltriaux 1b70024a29 # Python bullseye
* adds -bullseye to base docker package to ensure backwards compatibility
2024-01-05 17:38:58 +01:00
mpeltriaux 58206853ee Django5
* changes python dependency
2024-01-05 10:05:27 +01:00
mpeltriaux 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
mpeltriaux 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
mpeltriaux 5ac0654fd4 Merge pull request 'master' (#375) from master into Docker
Reviewed-on: SGD-Nord/konova#375
2023-12-13 13:38:00 +01:00
mpeltriaux 6c07a81b4f Merge pull request 'master' (#372) from master into Docker
Reviewed-on: SGD-Nord/konova#372
2023-12-12 07:35:17 +01:00
mpeltriaux 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
mpeltriaux 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
mpeltriaux 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
mpeltriaux 1996efcc0d Docker-compose fix
* drops local volume usage in favor of copied code into container
2023-11-30 12:44:50 +01:00
mpeltriaux 80569119cb Merge branch 'master' into Docker
# Conflicts:
#	requirements.txt
2023-11-30 12:43:44 +01:00
mpeltriaux 98e71d4e8a Merge pull request 'HOTFIX' (#355) from master into Docker
Reviewed-on: SGD-Nord/konova#355
2023-11-07 16:27:11 +01:00
mpeltriaux fec7191ac2 Merge pull request 'HOTFIX' (#354) from master into Docker
Reviewed-on: SGD-Nord/konova#354
2023-10-26 07:27:19 +02:00
mpeltriaux 9b1085f206 Merge pull request 'HOTFIX' (#353) from master into Docker
Reviewed-on: SGD-Nord/konova#353
2023-10-26 07:23:21 +02:00
mpeltriaux b35d175a5c Merge pull request 'master' (#351) from master into Docker
Reviewed-on: SGD-Nord/konova#351
2023-10-25 10:10:14 +02:00
mpeltriaux 7f5fb022ac Merge pull request 'master' (#348) from master into Docker
Reviewed-on: SGD-Nord/konova#348
2023-09-15 13:21:38 +02:00
mpeltriaux 2d3314ab18 Merge pull request 'master' (#344) from master into Docker
Reviewed-on: SGD-Nord/konova#344
2023-08-25 15:06:32 +02:00
mpeltriaux 8b489f013d Merge pull request 'master' (#341) from master into Docker
Reviewed-on: SGD-Nord/konova#341
2023-08-09 07:30:18 +02:00
mpeltriaux 16ce5506d8 Merge pull request 'master' (#336) from master into Docker
Reviewed-on: SGD-Nord/konova#336
2023-05-17 14:40:17 +02:00
mpeltriaux e440bf8372 Merge pull request 'master' (#333) from master into Docker
Reviewed-on: SGD-Nord/konova#333
2023-05-16 14:11:20 +02:00
mpeltriaux 607db267e6 Merge pull request 'master' (#330) from master into Docker
Reviewed-on: SGD-Nord/konova#330
2023-04-26 11:30:22 +02:00
mpeltriaux 352ca64e09 Merge pull request 'master' (#327) from master into Docker
Reviewed-on: SGD-Nord/konova#327
2023-04-19 15:25:05 +02:00
mpeltriaux f2b735da6e Merge pull request 'master' (#324) from master into Docker
Reviewed-on: SGD-Nord/konova#324
2023-03-30 15:12:47 +02:00
mpeltriaux 6f7cfb713e Merge pull request 'master' (#321) from master into Docker
Reviewed-on: SGD-Nord/konova#321
2023-03-28 13:53:14 +02:00
mpeltriaux 103b703ee9 Merge pull request 'master' (#318) from master into Docker
Reviewed-on: SGD-Nord/konova#318
2023-03-24 07:14:34 +01:00
mpeltriaux daf8b1dce6 Merge pull request 'master' (#316) from master into Docker
Reviewed-on: SGD-Nord/konova#316
2023-03-22 09:00:33 +01:00
mpeltriaux c088affd74 Merge pull request 'master' (#313) from master into Docker
Reviewed-on: SGD-Nord/konova#313
2023-03-16 08:14:37 +01:00
mpeltriaux ecc727c991 Merge pull request 'master' (#311) from master into Docker
Reviewed-on: SGD-Nord/konova#311
2023-03-13 07:00:49 +01:00
mpeltriaux 632569fa5d Merge pull request 'master' (#307) from master into Docker
Reviewed-on: SGD-Nord/konova#307
2023-02-23 15:35:07 +01:00
mpeltriaux 6c6cbb7396 Merge pull request 'HOTFIX' (#305) from master into Docker
Reviewed-on: SGD-Nord/konova#305
2023-02-23 12:03:23 +01:00
mpeltriaux 5e6bfdf77e Merge pull request 'HOTFIX' (#304) from master into Docker
Reviewed-on: SGD-Nord/konova#304
2023-02-23 10:45:57 +01:00
mpeltriaux 35e5e18b79 Merge pull request 'master' (#303) from master into Docker
Reviewed-on: SGD-Nord/konova#303
2023-02-23 10:24:38 +01:00
mpeltriaux c0e8c6bd84 Merge pull request 'master' (#298) from master into Docker
Reviewed-on: SGD-Nord/konova#298
2023-02-21 08:07:47 +01:00
mpeltriaux 64541b76c5 Merge pull request 'master' (#295) from master into Docker
Reviewed-on: SGD-Nord/konova#295
2023-02-13 14:42:17 +01:00
mpeltriaux f65b9262cb Merge pull request 'master' (#292) from master into Docker
Reviewed-on: SGD-Nord/konova#292
2023-02-06 15:01:50 +01:00
mpeltriaux 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
mpeltriaux 951f810ce5 Merge pull request 'master' (#287) from master into Docker
Reviewed-on: SGD-Nord/konova#287
2023-02-01 14:10:04 +01:00
mpeltriaux d2c177d448 Merge pull request 'master' (#283) from master into Docker
Reviewed-on: SGD-Nord/konova#283
2022-12-22 07:56:02 +01:00
mpeltriaux 299727a7b4 Merge pull request 'master' (#279) from master into Docker
Reviewed-on: SGD-Nord/konova#279
2022-12-14 16:37:41 +01:00
mpeltriaux b97976b2c5 Merge pull request 'master' (#276) from master into Docker
Reviewed-on: SGD-Nord/konova#276
2022-12-13 06:50:43 +01:00
mpeltriaux 20241661ff Merge pull request 'master' (#273) from master into Docker
Reviewed-on: SGD-Nord/konova#273
2022-12-09 13:02:46 +01:00
mpeltriaux ad5c0bea67 Merge pull request 'master' (#270) from master into Docker
Reviewed-on: SGD-Nord/konova#270
2022-12-09 07:25:30 +01:00
mpeltriaux 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
mpeltriaux 5c2b5affc9 Merge pull request 'master' (#264) from master into Docker
Reviewed-on: SGD-Nord/konova#264
2022-12-05 06:10:26 +01:00
mpeltriaux cd99743d1e Merge pull request 'master' (#261) from master into Docker
Reviewed-on: SGD-Nord/konova#261
2022-12-02 06:43:28 +01:00
mpeltriaux b39432be1a Merge pull request 'master' (#259) from master into Docker
Reviewed-on: SGD-Nord/konova#259
2022-12-01 14:01:40 +01:00
mpeltriaux 03f9a33e54 Merge pull request 'Hotfix' (#256) from master into Docker
Reviewed-on: SGD-Nord/konova#256
2022-12-01 07:05:50 +01:00
mpeltriaux 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
mpeltriaux 4dfd02291e Merge pull request 'master' (#253) from master into Docker
Reviewed-on: SGD-Nord/konova#253
2022-11-28 07:29:36 +01:00
mpeltriaux e7ca485a88 Merge pull request 'master' (#247) from master into Docker
Reviewed-on: SGD-Nord/konova#247
2022-11-23 16:07:21 +01:00
mpeltriaux 8319cbfe17 Merge pull request 'master' (#245) from master into Docker
Reviewed-on: SGD-Nord/konova#245
2022-11-23 07:14:15 +01:00
mpeltriaux 4a023e9f10 Merge pull request 'Hotfix' (#242) from master into Docker
Reviewed-on: SGD-Nord/konova#242
2022-11-18 16:22:50 +01:00
mpeltriaux 4100f96dc6 Merge pull request 'master' (#241) from master into Docker
Reviewed-on: SGD-Nord/konova#241
2022-11-18 16:17:39 +01:00
mpeltriaux ca24f098e4 Merge pull request 'master' (#236) from master into Docker
Reviewed-on: SGD-Nord/konova#236
2022-11-18 06:53:27 +01:00
mpeltriaux 80dcd62199 Merge pull request 'master' (#234) from master into Docker
Reviewed-on: SGD-Nord/konova#234
2022-11-17 06:55:28 +01:00
mpeltriaux 0cfd3da728 Merge pull request 'master' (#227) from master into Docker
Reviewed-on: SGD-Nord/konova#227
2022-11-14 07:23:43 +01:00
mpeltriaux e141851a87 Merge pull request 'master' (#224) from master into Docker
Reviewed-on: SGD-Nord/konova#224
2022-10-19 07:35:31 +02:00
mpeltriaux 89ec67999b Merge pull request 'master' (#221) from master into Docker
Reviewed-on: SGD-Nord/konova#221
2022-10-12 09:02:49 +02:00
mpeltriaux ec38daaedc Merge pull request 'master' (#219) from master into Docker
Reviewed-on: SGD-Nord/konova#219
2022-10-11 16:41:06 +02:00
mpeltriaux 45c0826a84 Merge pull request 'master' (#215) from master into Docker
Reviewed-on: SGD-Nord/konova#215
2022-10-05 11:03:03 +02:00
mpeltriaux 45a383cf85 Merge pull request 'master' (#213) from master into Docker
Reviewed-on: SGD-Nord/konova#213
2022-09-29 10:46:30 +02:00
mpeltriaux 90aff209f9 Merge pull request 'master' (#210) from master into Docker
Reviewed-on: SGD-Nord/konova#210
2022-09-28 12:28:49 +02:00
mpeltriaux 13528e91e9 Merge pull request 'master' (#207) from master into Docker
Reviewed-on: SGD-Nord/konova#207
2022-09-16 12:13:59 +02:00
mpeltriaux 04179d633c Merge pull request 'master' (#205) from master into Docker
Reviewed-on: SGD-Nord/konova#205
2022-09-16 07:24:21 +02:00
mpeltriaux 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
mpeltriaux 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
mpeltriaux af747417d3 Revert "Revert "Merge branch 'Docker' into master""
This reverts commit 1c38acea25.
2022-08-02 09:44:25 +02:00
mpeltriaux 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
38 changed files with 1155 additions and 505 deletions
+36
View File
@@ -0,0 +1,36 @@
# Nutze ein schlankes Python-Image
FROM python:3.13-slim-trixie
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"]
+56
View File
@@ -4,6 +4,7 @@ the database postgresql and the css library bootstrap as well as the icon packag
fontawesome for a modern look, following best practices from the industry. fontawesome for a modern look, following best practices from the industry.
## Background processes ## Background processes
### !!! For non-docker run
Konova uses celery for background processing. To start the worker you need to run Konova uses celery for background processing. To start the worker you need to run
```shell ```shell
$ celery -A konova worker -l INFO $ celery -A konova worker -l INFO
@@ -18,3 +19,58 @@ Technical documention is provided in the projects git wiki.
A user documentation is not available (and not needed, yet). A user documentation is not available (and not needed, yet).
# Docker
To run the docker-compose as expected, you need to take the following steps:
1. Create a database containing docker, using an appropriate Dockerfile, e.g. the following
```
version: '3.3'
services:
postgis:
image: postgis/postgis
restart: always
container_name: postgis-docker
ports:
- 5433:5432
volumes:
- db-volume:/var/lib/postgresql/data
environment:
- POSTGRES_PASSWORD=postgres
- POSTGRES_USER=postgres
networks:
- db-network-bridge
networks:
db-network-bridge:
driver: "bridge"
volumes:
db-volume:
```
This Dockerfile creates a Docker container running postgresql and postgis, creates the default superuser postgres,
creates a named volume for persisting the database and creates a new network bridge, which **must be used by any other
container, which wants to write/read on this database**.
2. Make sure the name of the network bridge above matches the network in the konova docker-compose.yml
3. Get into the running postgis container (`docker exec -it postgis-docker bash`) and create new databases, users and so on. Make sure the database `konova` exists now!
4. Replace all `CHANGE_ME_xy` values inside of konova/docker-compose.yml for your installation. Make sure the `SSO_HOST` holds the proper SSO host, e.g. for the arnova project `arnova.example.org` (Arnova must be installed and the webserver configured as well, of course)
5. Take a look on konova/settings.py and konova/sub_settings/django_settings.py. Again: Replace all occurences of `CHANGE_ME` with proper values for your installation.
1. Make sure you have the proper host strings added to `ALLOWED_HOSTS` inside of django_settings.py.
6. Build and run the docker setup using `docker-compose build` and `docker-compose start` from the main directory of this project (where the docker-compose.yml lives)
7. Run migrations! To do so, get into the konova service container (`docker exec -it konova-docker bash`) and run the needed commands (`python manage.py makemigrations LIST_OF_ALL_MIGRATABLE_APPS`, then `python manage.py migrate`)
8. Run the setup command `python manage.py setup` and follow the instructions on the CLI
9. To enable **SMTP** mail support, make sure your host machine (the one where the docker container run) has the postfix service configured properly. Make sure the `mynetworks` variable is xtended using the docker network bridge ip, created in the postgis container and used by the konova services.
1. **Hint**: You can find out this easily by trying to perform a test mail in the running konova web application (which will fail, of course). Then take a look to the latest entries in `/var/log/mail.log` on your host machine. The failed IP will be displayed there.
2. **Please note**: This installation guide is based on SMTP using postfix!
3. Restart the postfix service on your host machine to reload the new configuration (`service postfix restart`)
10. Finally, make sure your host machine webserver passes incoming requests properly to the docker nginx webserver of konova. A proper nginx config for the host machine may look like this:
```
server {
server_name konova.domain.org;
location / {
proxy_pass http://localhost:KONOVA_NGINX_DOCKER_PORT/;
proxy_set_header Host $host;
}
}
```
+12
View File
@@ -1,5 +1,6 @@
from django.contrib import admin from django.contrib import admin
from api.models import ExternalIdentifier
from api.models.token import APIUserToken, OAuthToken from api.models.token import APIUserToken, OAuthToken
@@ -28,6 +29,17 @@ class OAuthTokenAdmin(admin.ModelAdmin):
"refresh_token", "refresh_token",
] ]
class ExternalIdentifierAdmin(admin.ModelAdmin):
list_display = [
"external_id",
"internal_id",
"created",
]
search_fields = [
"external_id",
"internal_id",
]
admin.site.register(APIUserToken, APITokenAdmin) admin.site.register(APIUserToken, APITokenAdmin)
admin.site.register(OAuthToken, OAuthTokenAdmin) admin.site.register(OAuthToken, OAuthTokenAdmin)
admin.site.register(ExternalIdentifier, ExternalIdentifierAdmin)
@@ -1,5 +1,5 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
], ],
"properties": { "properties": {
@@ -1,5 +1,5 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
], ],
"properties": { "properties": {
@@ -1,5 +1,5 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
], ],
"properties": { "properties": {
@@ -1,5 +1,5 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
], ],
"properties": { "properties": {
@@ -1,44 +1,26 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
[ [
[ [
[ 7.489245186249832,
7.845568656921382, 50.29519575559556
50.79829702304368
], ],
[ [
7.837371826171871, 7.5244137991026605,
50.80155187891526 50.29519575559556
], ],
[ [
7.835698127746578, 7.5244137991026605,
50.805267562209806 50.275124048323875
], ],
[ [
7.841062545776364, 7.489245186249832,
50.806623577403386 50.275124048323875
], ],
[ [
7.848916053771969, 7.489245186249832,
50.808359219420474 50.29519575559556
],
[
7.855696678161618,
50.807057493952975
],
[
7.854666709899899,
50.80423696434001
],
[
7.850461006164548,
50.80217570040005
],
[
7.845568656921382,
50.79829702304368
]
] ]
] ]
], ],
@@ -1,44 +1,26 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
[ [
[ [
[ 7.489245186249832,
7.845568656921382, 50.29519575559556
50.79829702304368
], ],
[ [
7.837371826171871, 7.5244137991026605,
50.80155187891526 50.29519575559556
], ],
[ [
7.835698127746578, 7.5244137991026605,
50.805267562209806 50.275124048323875
], ],
[ [
7.841062545776364, 7.489245186249832,
50.806623577403386 50.275124048323875
], ],
[ [
7.848916053771969, 7.489245186249832,
50.808359219420474 50.29519575559556
],
[
7.855696678161618,
50.807057493952975
],
[
7.854666709899899,
50.80423696434001
],
[
7.850461006164548,
50.80217570040005
],
[
7.845568656921382,
50.79829702304368
]
] ]
] ]
], ],
+11 -29
View File
@@ -1,44 +1,26 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
[ [
[ [
[ 7.489245186249832,
7.845568656921382, 50.29519575559556
50.79829702304368
], ],
[ [
7.837371826171871, 7.5244137991026605,
50.80155187891526 50.29519575559556
], ],
[ [
7.835698127746578, 7.5244137991026605,
50.805267562209806 50.275124048323875
], ],
[ [
7.841062545776364, 7.489245186249832,
50.806623577403386 50.275124048323875
], ],
[ [
7.848916053771969, 7.489245186249832,
50.808359219420474 50.29519575559556
],
[
7.855696678161618,
50.807057493952975
],
[
7.854666709899899,
50.80423696434001
],
[
7.850461006164548,
50.80217570040005
],
[
7.845568656921382,
50.79829702304368
]
] ]
] ]
], ],
@@ -1,44 +1,26 @@
{ {
"type": "MultiPolygon", "type": "Polygon",
"coordinates": [ "coordinates": [
[ [
[ [
[ 7.489245186249832,
7.845568656921382, 50.29519575559556
50.79829702304368
], ],
[ [
7.837371826171871, 7.5244137991026605,
50.80155187891526 50.29519575559556
], ],
[ [
7.835698127746578, 7.5244137991026605,
50.805267562209806 50.275124048323875
], ],
[ [
7.841062545776364, 7.489245186249832,
50.806623577403386 50.275124048323875
], ],
[ [
7.848916053771969, 7.489245186249832,
50.808359219420474 50.29519575559556
],
[
7.855696678161618,
50.807057493952975
],
[
7.854666709899899,
50.80423696434001
],
[
7.850461006164548,
50.80217570040005
],
[
7.845568656921382,
50.79829702304368
]
] ]
] ]
], ],
+10 -9
View File
@@ -9,10 +9,11 @@ Created on: 28.01.22
import json import json
from django.contrib.gis import geos from django.contrib.gis import geos
from django.contrib.gis.geos import MultiPolygon
from django.urls import reverse from django.urls import reverse
from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase from api.tests.v1.share.test_api_sharing import BaseAPIV1TestCase
from konova.models import Geometry from konova.utils.geometry.geometry_validator import GeometryProcessor
class APIV1UpdateTestCase(BaseAPIV1TestCase): class APIV1UpdateTestCase(BaseAPIV1TestCase):
@@ -64,8 +65,8 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"] put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body)) put_geom = geos.fromstr(json.dumps(put_body))
put_geom = Geometry.cast_to_rlp_srid(put_geom) put_geom = MultiPolygon(GeometryProcessor.cast_to_rlp_srid(put_geom))
self.assertEqual(put_geom, self.intervention.geometry.geom) self.assertTrue(put_geom.equals_exact(self.intervention.geometry.geom,0.00001))
self.assertEqual(put_props["title"], self.intervention.title) self.assertEqual(put_props["title"], self.intervention.title)
self.assertNotEqual(modified_on, self.intervention.modified) self.assertNotEqual(modified_on, self.intervention.modified)
self.assertEqual(put_props["responsible"]["registration_file_number"], self.intervention.responsible.registration_file_number) self.assertEqual(put_props["responsible"]["registration_file_number"], self.intervention.responsible.registration_file_number)
@@ -94,8 +95,8 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"] put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body)) put_geom = geos.fromstr(json.dumps(put_body))
put_geom = Geometry.cast_to_rlp_srid(put_geom) put_geom = MultiPolygon(GeometryProcessor.cast_to_rlp_srid(put_geom))
self.assertEqual(put_geom, self.compensation.geometry.geom) self.assertTrue(put_geom.equals_exact(self.compensation.geometry.geom, 0.00001))
self.assertEqual(put_props["title"], self.compensation.title) self.assertEqual(put_props["title"], self.compensation.title)
self.assertNotEqual(modified_on, self.compensation.modified) self.assertNotEqual(modified_on, self.compensation.modified)
self.assertEqual(put_props["is_cef"], self.compensation.is_cef) self.assertEqual(put_props["is_cef"], self.compensation.is_cef)
@@ -124,8 +125,8 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"] put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body)) put_geom = geos.fromstr(json.dumps(put_body))
put_geom = Geometry.cast_to_rlp_srid(put_geom) put_geom = MultiPolygon(GeometryProcessor.cast_to_rlp_srid(put_geom))
self.assertEqual(put_geom, self.eco_account.geometry.geom) self.assertTrue(put_geom.equals_exact(self.eco_account.geometry.geom, 0.00001))
self.assertEqual(put_props["title"], self.eco_account.title) self.assertEqual(put_props["title"], self.eco_account.title)
self.assertNotEqual(modified_on, self.eco_account.modified) self.assertNotEqual(modified_on, self.eco_account.modified)
self.assertEqual(put_props["deductable_surface"], str(self.eco_account.deductable_surface)) self.assertEqual(put_props["deductable_surface"], str(self.eco_account.deductable_surface))
@@ -156,8 +157,8 @@ class APIV1UpdateTestCase(BaseAPIV1TestCase):
put_props = put_body["properties"] put_props = put_body["properties"]
put_geom = geos.fromstr(json.dumps(put_body)) put_geom = geos.fromstr(json.dumps(put_body))
put_geom = Geometry.cast_to_rlp_srid(put_geom) put_geom = MultiPolygon(GeometryProcessor.cast_to_rlp_srid(put_geom))
self.assertEqual(put_geom, self.ema.geometry.geom) self.assertTrue(put_geom.equals_exact(self.ema.geometry.geom,0.00001))
self.assertEqual(put_props["title"], self.ema.title) self.assertEqual(put_props["title"], self.ema.title)
self.assertNotEqual(modified_on, self.ema.modified) self.assertNotEqual(modified_on, self.ema.modified)
self.assertEqual(put_props["responsible"]["conservation_office"], self.ema.responsible.conservation_office) self.assertEqual(put_props["responsible"]["conservation_office"], self.ema.responsible.conservation_office)
+30 -8
View File
@@ -6,16 +6,19 @@ Created on: 24.01.22
""" """
import json import json
import uuid
from abc import abstractmethod from abc import abstractmethod
from django.contrib.gis import geos from django.contrib.gis import geos
from django.contrib.gis.geos import GEOSGeometry from django.contrib.gis.geos import GEOSGeometry
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist, ValidationError
from django.core.paginator import Paginator from django.core.paginator import Paginator
from django.db.models import Q from django.db.models import Q
from api.models import ExternalIdentifier from api.models import ExternalIdentifier
from konova.models import Geometry from konova.models import Geometry
from konova.sub_settings.lanis_settings import DEFAULT_SRID
from konova.utils.geometry.geometry_validator import GeoJsonValidator
from konova.utils.message_templates import DATA_UNSHARED from konova.utils.message_templates import DATA_UNSHARED
@@ -101,6 +104,7 @@ class AbstractModelAPISerializer:
Returns: Returns:
serialized_data (dict) serialized_data (dict)
""" """
try:
entries = self.model.objects.filter( entries = self.model.objects.filter(
**self.lookup **self.lookup
).filter( ).filter(
@@ -108,6 +112,8 @@ class AbstractModelAPISerializer:
).order_by( ).order_by(
"id" "id"
).distinct() ).distinct()
except ValidationError:
raise AssertionError("ID expected to be of UUID4 for internal lookup.")
self.paginator = Paginator(entries, self.rpp) self.paginator = Paginator(entries, self.rpp)
requested_entries = self.paginator.page(self.page_number) requested_entries = self.paginator.page(self.page_number)
@@ -152,11 +158,20 @@ class AbstractModelAPISerializer:
Returns: Returns:
geometry (GEOSGeometry) geometry (GEOSGeometry)
""" """
if isinstance(geojson, dict): if isinstance(geojson, str):
geojson = json.dumps(geojson) geojson = json.loads(geojson)
geometry = geos.fromstr(geojson)
geometry = Geometry.cast_to_rlp_srid(geometry) geojson_validator = GeoJsonValidator(geojson, DEFAULT_SRID)
geometry = Geometry.cast_to_multipolygon(geometry) geojson_validator.validate()
if geojson_validator.errors:
raise ValueError(geojson_validator.errors)
geometry = geojson_validator.validated_geometry
is_input_geometry_too_small = geometry.area == 0.0 and geojson_validator.num_ignored_geometries > 0
if is_input_geometry_too_small:
raise ValueError(f"Your area was too small to be processed! Must be > 1m²!")
return geometry return geometry
def _get_obj_from_db(self, id, user): def _get_obj_from_db(self, id, user):
@@ -171,13 +186,20 @@ class AbstractModelAPISerializer:
Returns: Returns:
""" """
if not id:
raise ValueError("No id provided. The id is expected to live in the URL!")
# First if there is an external identifier linked to an internal one, so we can continue with the internal # First if there is an external identifier linked to an internal one, so we can continue with the internal
try: try:
ext_id = ExternalIdentifier.objects.get(external_id=id) ext_id = ExternalIdentifier.objects.get(external_id=id)
id = ext_id.internal_id id = ext_id.internal_id
except ObjectDoesNotExist: except ObjectDoesNotExist:
# No external id found - let's hope the given id exists internally # Not found as external id - let's check whether this is a valid uuid and therefore
pass # potentially an internal id
try:
uuid.UUID(id)
except ValueError:
raise AssertionError(f"'{id}' is neither a known external identifier nor a valid uuid.")
obj = self.model.objects.get( obj = self.model.objects.get(
id=id, id=id,
+15 -18
View File
@@ -10,6 +10,7 @@ from json import JSONDecodeError
from django.core.exceptions import ObjectDoesNotExist from django.core.exceptions import ObjectDoesNotExist
from django.http import JsonResponse, HttpRequest from django.http import JsonResponse, HttpRequest
from django.views.decorators.csrf import csrf_exempt
from api.utils.serializer.v1.compensation import CompensationAPISerializerV1 from api.utils.serializer.v1.compensation import CompensationAPISerializerV1
from api.utils.serializer.v1.deduction import DeductionAPISerializerV1 from api.utils.serializer.v1.deduction import DeductionAPISerializerV1
@@ -28,6 +29,20 @@ class AbstractAPIViewV1(AbstractAPIView):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.serializer = self.serializer() self.serializer = self.serializer()
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
try:
return super().dispatch(request, *args, **kwargs)
except (JSONDecodeError,
AssertionError,
ValueError,
PermissionError) as e:
return self._return_error_response(e, 400)
except ObjectDoesNotExist as e:
return self._return_error_response(e, 404)
except Exception as e:
return self._return_error_response(e, 500)
def get(self, request: HttpRequest, id=None): def get(self, request: HttpRequest, id=None):
""" Handles the GET request """ Handles the GET request
@@ -40,17 +55,12 @@ class AbstractAPIViewV1(AbstractAPIView):
Returns: Returns:
response (JsonResponse) response (JsonResponse)
""" """
try:
self.rpp = int(request.GET.get("rpp", self.rpp)) self.rpp = int(request.GET.get("rpp", self.rpp))
self.page_number = int(request.GET.get("p", self.page_number)) self.page_number = int(request.GET.get("p", self.page_number))
self.serializer.rpp = self.rpp self.serializer.rpp = self.rpp
self.serializer.page_number = self.page_number self.serializer.page_number = self.page_number
self.serializer.prepare_lookup(id, self.user) self.serializer.prepare_lookup(id, self.user)
data = self.serializer.fetch_and_serialize() data = self.serializer.fetch_and_serialize()
except Exception as e:
return self._return_error_response(e, 500)
return self._return_response(request, data) return self._return_response(request, data)
def post(self, request: HttpRequest): def post(self, request: HttpRequest):
@@ -64,16 +74,9 @@ class AbstractAPIViewV1(AbstractAPIView):
Returns: Returns:
response (JsonResponse) response (JsonResponse)
""" """
try:
body = request.body.decode("utf-8") body = request.body.decode("utf-8")
body = json.loads(body) body = json.loads(body)
created_id = self.serializer.create_model_from_json(body, self.user) created_id = self.serializer.create_model_from_json(body, self.user)
except (JSONDecodeError,
AssertionError,
ValueError,
PermissionError,
ObjectDoesNotExist) as e:
return self._return_error_response(e, 400)
return JsonResponse({"id": created_id}) return JsonResponse({"id": created_id})
def put(self, request: HttpRequest, id=None): def put(self, request: HttpRequest, id=None):
@@ -88,12 +91,9 @@ class AbstractAPIViewV1(AbstractAPIView):
Returns: Returns:
response (JsonResponse) response (JsonResponse)
""" """
try:
body = request.body.decode("utf-8") body = request.body.decode("utf-8")
body = json.loads(body) body = json.loads(body)
updated_id = self.serializer.update_model_from_json(id, body, self.user) updated_id = self.serializer.update_model_from_json(id, body, self.user)
except Exception as e:
return self._return_error_response(e, 500)
return JsonResponse({"id": updated_id}) return JsonResponse({"id": updated_id})
def delete(self, request: HttpRequest, id=None): def delete(self, request: HttpRequest, id=None):
@@ -107,10 +107,7 @@ class AbstractAPIViewV1(AbstractAPIView):
response (JsonResponse) response (JsonResponse)
""" """
try:
success = self.serializer.delete_entry(id, self.user) success = self.serializer.delete_entry(id, self.user)
except Exception as e:
return self._return_error_response(e, 500)
return JsonResponse( return JsonResponse(
{ {
"success": success, "success": success,
+2 -2
View File
@@ -325,8 +325,8 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
self.identifier = self.generate_new_identifier() self.identifier = self.generate_new_identifier()
super().save(*args, **kwargs) super().save(*args, **kwargs)
def mark_as_deleted(self, user, send_mail: bool = True): def mark_as_deleted(self, user, send_mail: bool = True, comment: str|None = None):
super().mark_as_deleted(user, send_mail) super().mark_as_deleted(user, send_mail, comment)
if user is not None: if user is not None:
self.intervention.mark_as_edited(user, edit_comment=COMPENSATION_REMOVED_TEMPLATE.format(self.identifier)) self.intervention.mark_as_edited(user, edit_comment=COMPENSATION_REMOVED_TEMPLATE.format(self.identifier))
@@ -30,17 +30,45 @@
{% endif %} {% endif %}
</td> </td>
</tr> </tr>
<tr>
<th scope="row">{% trans 'Available' %}</th>
<td>
{{available_total|floatformat:2}} / {{obj.deductable_surface|default_if_none:0.00|floatformat:2}} m²
{% with available as value %}
{% include 'konova/widgets/progressbar.html' %}
{% endwith %}
</td>
</tr>
<tr> <tr>
<th scope="row">{% trans 'Deductions for' %}</th> <th scope="row">{% trans 'Deductions for' %}</th>
<td class="align-middle"> <td class="align-middle">
<table class="table table-hover">
<th scope="col">
{% translate 'Intervention' %}
</th>
<th scope="col">
{% translate 'Amount' %}
</th>
{% for deduction in deductions %} {% for deduction in deductions %}
<tr>
<td>
<a href="{% url 'intervention:report' deduction.intervention__id %}"> <a href="{% url 'intervention:report' deduction.intervention__id %}">
{{deduction.intervention__identifier}} - {{deduction.intervention__title}} {{deduction.intervention__identifier}} - {{deduction.intervention__title}}
</a> </a>
<br> </td>
<td>{{ deduction.surface|floatformat:2 }} m²</td>
</tr>
{% empty %} {% empty %}
{% trans 'None' %} <tr>
<td>
{% translate 'None' %}
</td>
<td>
/
</td>
</tr>
{% endfor %} {% endfor %}
</table>
</td> </td>
</tr> </tr>
<tr> <tr>
+11 -1
View File
@@ -66,7 +66,15 @@ class EcoAccountPublicReportView(AbstractPublicReportView):
deductions = acc.deductions.all() \ deductions = acc.deductions.all() \
.distinct("intervention") \ .distinct("intervention") \
.select_related("intervention") \ .select_related("intervention") \
.values_list("intervention__id", "intervention__identifier", "intervention__title", named=True) .values_list("intervention__id",
"intervention__identifier",
"intervention__title",
"surface",
named=True)
# Calculate rest of available surface for deductions
available_total = acc.deductable_rest
available_relative = acc.get_deductable_rest_relative()
context = { context = {
"obj": acc, "obj": acc,
@@ -86,6 +94,8 @@ class EcoAccountPublicReportView(AbstractPublicReportView):
"actions": actions, "actions": actions,
"deductions": deductions, "deductions": deductions,
"tables_scrollable": False, "tables_scrollable": False,
"available": available_relative,
"available_total": available_total,
TAB_TITLE_IDENTIFIER: tab_title, TAB_TITLE_IDENTIFIER: tab_title,
} }
context = BaseContext(request, context).context context = BaseContext(request, context).context
+21
View File
@@ -0,0 +1,21 @@
services:
konova:
external_links:
- postgis:db
- arnova-nginx-server:arnova
build: .
image: "ksp/konova:x.y"
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:
name: postgis_nat_it_backend
external: true
+27
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"'
+1 -1
View File
@@ -1,6 +1,6 @@
from django.contrib import admin from django.contrib import admin
from intervention.models import Intervention, Responsibility, Legal, Revocation, InterventionDocument from intervention.models import Intervention
from konova.admin import AbstractDocumentAdmin, BaseObjectAdmin from konova.admin import AbstractDocumentAdmin, BaseObjectAdmin
+3 -2
View File
@@ -306,7 +306,7 @@ class Intervention(BaseObject,
self.set_unchecked() self.set_unchecked()
return action return action
def mark_as_deleted(self, user, send_mail: bool = True): def mark_as_deleted(self, user, send_mail: bool = True, comment: str|None = None):
""" Extends base mark_as_delete functionality """ Extends base mark_as_delete functionality
Removes related deductions from the database, which results in updating the deductable_rest of the Removes related deductions from the database, which results in updating the deductable_rest of the
@@ -315,11 +315,12 @@ class Intervention(BaseObject,
Args: Args:
user (User): The performing user user (User): The performing user
send_mail (bool): Whether to send an info mail send_mail (bool): Whether to send an info mail
comment (str|None): The delete comment
Returns: Returns:
""" """
super().mark_as_deleted(user, send_mail) super().mark_as_deleted(user, send_mail, comment)
# Remove pending deductions to free booked capacities # Remove pending deductions to free booked capacities
deductions = self.deductions.all() deductions = self.deductions.all()
+1 -3
View File
@@ -7,10 +7,7 @@ Created on: 22.07.21
""" """
from django.contrib import admin from django.contrib import admin
from konova.models import Geometry, Deadline, GeometryConflict, Parcel, District, Municipal, ParcelGroup, Resubmission
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.message_templates import COMPENSATION_REMOVED_TEMPLATE
from user.models import UserAction
class GeometryAdmin(admin.ModelAdmin): class GeometryAdmin(admin.ModelAdmin):
@@ -123,6 +120,7 @@ class BaseResourceAdmin(admin.ModelAdmin):
class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin): class BaseObjectAdmin(BaseResourceAdmin, DeletableObjectMixinAdmin):
search_fields = [ search_fields = [
"id",
"identifier", "identifier",
"title", "title",
] ]
+17 -120
View File
@@ -7,9 +7,7 @@ Created on: 15.08.22
""" """
import json import json
from django.contrib.gis import gdal from django.contrib.gis.geos import MultiPolygon
from django.contrib.gis.geos import MultiPolygon, Polygon
from django.contrib.gis.geos.prototypes.io import WKTWriter
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.forms import JSONField from django.forms import JSONField
@@ -18,6 +16,7 @@ from konova.models import Geometry
from konova.settings import GEOM_MAX_VERTICES from konova.settings import GEOM_MAX_VERTICES
from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts from konova.tasks import celery_update_parcels, celery_check_for_geometry_conflicts
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.geometry.geometry_validator import GeoJsonValidator, GeometryProcessor
from user.models import UserActionLogEntry from user.models import UserActionLogEntry
@@ -60,6 +59,11 @@ class SimpleGeomForm(BaseForm):
self.initialize_form_field("output", geom) self.initialize_form_field("output", geom)
def is_valid(self): def is_valid(self):
""" Custom is_valid method for SimpleGeomForm
Returns:
"""
super().is_valid() super().is_valid()
is_valid = True is_valid = True
@@ -75,118 +79,21 @@ class SimpleGeomForm(BaseForm):
geom = self.data.get("output", json.dumps({})) geom = self.data.get("output", json.dumps({}))
geom = json.loads(geom) geom = json.loads(geom)
# Initialize features list with empty MultiPolygon, so that an empty input will result in a geojson_validator = GeoJsonValidator(geom, DEFAULT_SRID_RLP)
# proper empty MultiPolygon object geojson_validator.validate()
features = []
features_json = geom.get("features", [])
accepted_ogr_types = [
"Polygon",
"Polygon25D",
"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 if geojson_validator.errors:
feature_geom = json.dumps(feature_geom) self.add_error("output", geojson_validator.errors)
g = gdal.OGRGeometry(feature_geom, srs=DEFAULT_SRID_RLP) return False
geometry_has_unwanted_dimensions = g.coord_dim > 2 # If cleaned_data is not proper empty dict, initialize one
if geometry_has_unwanted_dimensions:
g = self.__flatten_geom_to_2D(g)
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_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
# Whatever this geometry object is -> try to create a Polygon from it
# The resulting polygon object automatically detects whether a valid polygon has been created or not
g = Polygon.from_ewkt(g.ewkt)
is_valid &= g.valid
if not g.valid:
self.add_error("output", g.valid_reason)
return is_valid
# If the resulting polygon is just a single polygon, we add it to the list of properly casted features
if isinstance(g, Polygon):
features.append(g)
elif isinstance(g, MultiPolygon):
# The resulting polygon could be of type MultiPolygon (due to multiple surfaces)
# If so, we extract all polygons from the MultiPolygon and extend the casted features list
features.extend(list(g))
# Unionize all polygon features into one new MultiPolygon
if features:
form_geom = MultiPolygon(*features, srid=DEFAULT_SRID_RLP).unary_union
else:
# If no features have been processed, this indicates an empty geometry - so we store an empty geometry
form_geom = MultiPolygon(srid=DEFAULT_SRID_RLP)
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
form_geom = Geometry.cast_to_multipolygon(form_geom)
# Write unionized Multipolygon back into cleaned data
if self.cleaned_data is None: if self.cleaned_data is None:
self.cleaned_data = {} self.cleaned_data = {}
self.cleaned_data["output"] = form_geom.ewkt self.cleaned_data["output"] = geojson_validator.validated_geometry.ewkt
self._num_geometries_ignored = geojson_validator.num_ignored_geometries
return is_valid return is_valid
def __is_vertices_num_valid(self):
""" Checks whether the number of vertices in the geometry is not too high
Returns:
"""
geom = self.cleaned_data.get("output")
g = gdal.OGRGeometry(geom, srs=DEFAULT_SRID_RLP)
num_vertices = g.num_coords
return num_vertices <= GEOM_MAX_VERTICES
def __is_area_valid(self, geom: gdal.OGRGeometry):
""" Checks whether the area is at least > 1m²
Returns:
"""
is_area_valid = geom.area > 1 # > 1m² (SRID:25832)
return is_area_valid
def __simplify_geometry(self, geom, max_vert: int):
""" Simplifies a geometry
Geometry will be simplified until a threshold of max vertices has been reached.
Args:
geom (MultiPolygon): The geometry
max_vert (int): Threshold of maximum vertices in geometry
Returns:
geom (MultiPolygon): The simplified geometry
"""
tolerance = 0.1
n = geom.num_coords
while(n > max_vert):
geom = geom.simplify(tolerance)
n = geom.num_coords
tolerance += 0.1
return geom
def save(self, action: UserActionLogEntry): def save(self, action: UserActionLogEntry):
""" Saves the form's geometry """ Saves the form's geometry
@@ -213,9 +120,9 @@ class SimpleGeomForm(BaseForm):
created=action, created=action,
) )
is_vertices_num_valid = self.__is_vertices_num_valid() is_vertices_num_valid = GeometryProcessor.is_vertices_num_valid(geometry.geom)
if not is_vertices_num_valid: if not is_vertices_num_valid:
geometry.geom = self.__simplify_geometry(geometry.geom, max_vert=GEOM_MAX_VERTICES) geometry.geom = GeometryProcessor.simplify_geometry(geometry.geom, max_vert=GEOM_MAX_VERTICES)
geometry.save() geometry.save()
self._geometry_simplified = True self._geometry_simplified = True
@@ -240,16 +147,6 @@ class SimpleGeomForm(BaseForm):
""" """
return self._geometry_simplified return self._geometry_simplified
def __flatten_geom_to_2D(self, geom):
"""
Enforces a given OGRGeometry from higher dimensions into 2D
"""
wkt_w = WKTWriter(dim=2)
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): def _set_geojson_properties(self, geojson: dict, title: str = None):
""" Toggles the editable property of the geojson for proper handling in map client """ Toggles the editable property of the geojson for proper handling in map client
@@ -0,0 +1,211 @@
"""
Author: Michel Peltriaux
Created on: 16.07.26
"""
from django.db.models import QuerySet
from compensation.models import Compensation, EcoAccount
from ema.models import Ema
from intervention.models import Intervention
from konova.management.commands.setup import BaseKonovaCommand
from user.models import User
class Command(BaseKonovaCommand):
help = "Mass-deletes entries according to user input"
_DELETE_CLS = None
_DELETE_COMMENT = None
_DELETE_USER = None
_DELETE_FROM_DB = False
def handle(self, *args, **options):
self._get_delete_user()
self._get_object_type()
filter = self._get_filter()
self._get_delete_comment()
self._show_config(filter)
entries = self._collect_entries(filter)
self._process_delete(entries)
def _get_delete_user(self):
self._DELETE_USER = User.objects.filter(
is_superuser=True,
is_staff=True
).order_by(
"id"
).first()
if not self._DELETE_USER:
self._write_error("No admin-staff user could be found. Please check the admin users!")
def _get_object_type(self):
object_types = {
"eiv": Intervention,
"kom": Compensation,
"oek": EcoAccount,
"ema": Ema,
}
object_types_str = "|".join(object_types.keys())
object_type_select = input(f"Which object type shall be processed? ({object_types_str}): ").lower()
try:
self._DELETE_CLS = object_types[object_type_select]
if self._DELETE_CLS == EcoAccount:
oek_deduction_remove_understood = input(f"Please be aware that deleting OEKs will result in TOTAL REMOVING of their deductions without any chance to restore them. Do you want to continue? (y|n): ")
oek_deduction_remove_understood = oek_deduction_remove_understood.lower() == "y"
if not oek_deduction_remove_understood:
self._write_error("Removing of OEK canceled. Abort.")
exit(-1)
except KeyError:
self._write_error(f"'{object_type_select}' not a valid option of {object_types_str}. Abort.")
exit(-1)
def _get_filter(self):
filter_attribute = input("What do you want to filter entries by? (identifier|created_by_user): ").lower()
filter = {}
try:
assert filter_attribute in ["identifier", "created_by_user"]
if filter_attribute == "identifier":
filter_attribute = "identifier__contains"
elif filter_attribute == "created_by_user":
filter_attribute = "created__user__username"
except AssertionError:
self._write_error(f"'{filter_attribute}' unknown. Abort")
exit(-1)
filter_val = input("Use this value to search for: ")
try:
assert len(filter_val) > 0
except AssertionError:
self._write_error(f"'{filter_val}' is empty. Abort.")
exit(-1)
filter[filter_attribute] = filter_val
return filter
def _get_delete_comment(self):
self._DELETE_COMMENT = input("Comment to store in delete action: ")
def _show_config(self, filter: dict):
assert self._DELETE_CLS is not None
assert self._DELETE_COMMENT is not None
self._write_warning("You are about to delete entries with:")
self._write_warning(f" Object type: {self._DELETE_CLS.__name__}")
self._write_warning(f" Filter by: '{filter}'")
self._write_warning(f" Delete performing user: '{self._DELETE_USER}'")
def _collect_entries(self, filter: dict):
return self._DELETE_CLS.objects.filter(
deleted__isnull=True
).filter(
**filter
)
def _process_delete(self, entries: QuerySet):
""" Wrapping delete method. Forces user to decide whether entries will be marked as deleted or shall
be removed completely from database
Args:
entries (QuerySet): Entries to be processed
Returns:
"""
delete_type = input("Do you want 1) mark entries as deleted or 2) remove them from the database? (1|2): ")
try:
delete_type = int(delete_type)
assert delete_type in (1, 2)
self._DELETE_FROM_DB = delete_type == 2
except (ValueError, AssertionError) as e:
self._write_error(f"Entered unknown '{delete_type}'. Abort.")
exit(-1)
if self._DELETE_FROM_DB:
self._process_delete_from_db(entries)
else:
self._process_mark_as_deleted(entries)
def _process_mark_as_deleted(self, entries: QuerySet):
""" Method do mark chosen entries as deleted
Args:
entries (QuerySet): Entries to be processed
Returns:
"""
self._write_warning(f" Comment for delete action: '{self._DELETE_COMMENT}'")
self._check_entries_sample(entries)
entries_count = entries.count()
i = 0
for element in entries:
if i % 10 == 0:
self._write_warning(f" Deleted {i}/{entries_count}")
element.mark_as_deleted(user=self._DELETE_USER, send_mail=False, comment=self._DELETE_COMMENT)
i += 1
self._write_warning(f" Deleted {entries_count}/{entries_count}")
self._write_success("Entries deleted. Bye.")
def _process_delete_from_db(self, entries: QuerySet):
""" Method do REMOVE entries from the database
Args:
entries (QuerySet): Entries to be processed
Returns:
"""
self._check_entries_sample(entries)
entries_count = entries.count()
input_num_entries = input(f"THIS IS THE FINAL WARNING! {entries_count} ENTRIES WILL BE REMOVED FROM THE DATABASE! ENTER THE NUMBER OF ENTRIES TO BE DELETED TO CONTINUE: ")
try:
input_num_entries = int(input_num_entries)
except ValueError:
self._write_error(f"'{input_num_entries}' is not an integer. Abort.")
exit(-1)
if entries_count == input_num_entries:
i = 0
for element in entries:
if i % 10 == 0:
self._write_warning(f" Deleted {i}/{entries_count}")
element.delete() # call individual delete routine to enforce custom delete logic (not called on bulk deleting!)
i += 1
self._write_warning(f" Deleted {entries_count}/{entries_count}")
self._write_success("Entries deleted. Bye.")
else:
self._write_error(f"'{input_num_entries}' was not correct. Abort.")
exit(-1)
def _check_entries_sample(self, entries):
""" Shows a sample from the entries on the terminal, so the user can decide whether the results are as expected
Args:
entries (QuerySet): Entries to be processed
Returns:
"""
entries_count = entries.count()
if entries_count == 0:
self._write_success(f"Found {entries_count} matches to delete. Nothing to do here. Bye.")
exit(0)
self._write_warning(
f"Found {entries_count} matches to delete. Please double-check whether these samples from the results are what you expected: ")
first_three_elements = entries[:3]
for element in first_three_elements:
self._write_warning(f" {element.identifier} ({element.id})")
delete_input = input("Are you ready to delete them now? (y|n): ").lower()
delete_now = delete_input == "y"
if not delete_now:
self._write_error(f"Entered '{delete_input}'. Abort.")
exit(-1)
+7 -35
View File
@@ -113,7 +113,7 @@ class Geometry(BaseResource):
objs (list): The list of objects objs (list): The list of objects
""" """
objs = [] objs = []
stop_searching = False
# Some related data sets can be processed rather easily # Some related data sets can be processed rather easily
regular_sets = [ regular_sets = [
self.intervention_set, self.intervention_set,
@@ -129,10 +129,15 @@ class Geometry(BaseResource):
else: else:
objs += set_objs objs += set_objs
stop_searching = len(objs) > 0
if stop_searching:
break
if not stop_searching:
# ... but we need a special treatment for compensations, since they can be deleted directly OR inherit their # ... 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 # de-facto-deleted status from their deleted parent intervention
comp_objs = self.compensation_set.filter( comp_objs = self.compensation_set.filter(
Q(deleted=None) & Q(intervention__deleted=None) Q(deleted=None) | Q(intervention__deleted=None)
) )
if limit_to_attrs: if limit_to_attrs:
objs += comp_objs.values(*limit_to_attrs) objs += comp_objs.values(*limit_to_attrs)
@@ -414,39 +419,6 @@ class Geometry(BaseResource):
return complexity_factor 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
def get_conflict_geometries(self): def get_conflict_geometries(self):
""" Getter for geometry ids which conflict with this geometry or are conflicted by this one """ Getter for geometry ids which conflict with this geometry or are conflicted by this one
+31 -2
View File
@@ -92,13 +92,15 @@ class DeletableObjectMixin(models.Model):
class Meta: class Meta:
abstract = True abstract = True
def mark_as_deleted(self, user, send_mail: bool = True): def mark_as_deleted(self, user, send_mail: bool = True, comment: str|None = None):
""" Mark an entry as deleted """ Mark an entry as deleted
Does not delete from database but sets a timestamp for being deleted on and which user deleted the object Does not delete from database but sets a timestamp for being deleted on and which user deleted the object
Args: Args:
user (User): The performing user user (User): The performing user
send_mail (bool): Whether to send mails to users and teams
comment (str|None): The comment for delete action
Returns: Returns:
@@ -109,7 +111,7 @@ class DeletableObjectMixin(models.Model):
return return
with transaction.atomic(): with transaction.atomic():
action = UserActionLogEntry.get_deleted_action(user) action = UserActionLogEntry.get_deleted_action(user, comment)
self.deleted = action self.deleted = action
self.log.add(action) self.log.add(action)
@@ -144,6 +146,33 @@ class BaseObject(BaseResource, DeletableObjectMixin):
class Meta: class Meta:
abstract = True abstract = True
def delete(self, using=None, keep_parents=False):
""" Base deleting of a BaseObject instance
Args:
using:
keep_parents:
Returns:
"""
self.__delete_external_identifier()
super().delete(using, keep_parents)
def __delete_external_identifier(self):
""" Checks on existing external identifier linking and drops them
Returns:
"""
from api.models import ExternalIdentifier
try:
external_identifier = ExternalIdentifier.objects.filter(internal_id=self.id)
external_identifier.delete()
except ObjectDoesNotExist:
pass
@abstractmethod @abstractmethod
def set_status_messages(self, request: HttpRequest): def set_status_messages(self, request: HttpRequest):
raise NotImplementedError raise NotImplementedError
+2 -3
View File
@@ -191,11 +191,10 @@ STATICFILES_DIRS = [
] ]
# EMAIL (see https://docs.djangoproject.com/en/dev/topics/email/) # EMAIL (see https://docs.djangoproject.com/en/dev/topics/email/)
# CHANGE_ME !!! ONLY FOR DEVELOPMENT !!!
if DEBUG: if DEBUG:
# ONLY FOR DEVELOPMENT NEEDED
EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend' EMAIL_BACKEND = 'django.core.mail.backends.filebased.EmailBackend'
EMAIL_FILE_PATH = '/tmp/app-messages' # change this to a proper location EMAIL_FILE_PATH = '/tmp/app-messages'
DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL") # The default email address for the 'from' element DEFAULT_FROM_EMAIL = env("DEFAULT_FROM_EMAIL") # The default email address for the 'from' element
SERVER_EMAIL = DEFAULT_FROM_EMAIL # The default email sender address, which is used by Django to send errors via mail SERVER_EMAIL = DEFAULT_FROM_EMAIL # The default email sender address, which is used by Django to send errors via mail
+5 -14
View File
@@ -85,12 +85,6 @@ class GeometryTestCase(BaseTestCase):
) )
def test_get_data_objects(self): def test_get_data_objects(self):
num_objs_with_geom = 0
self.assertEqual(
len(self.geom_1.get_data_objects()),
num_objs_with_geom
)
objs = [ objs = [
self.intervention, self.intervention,
self.compensation, self.compensation,
@@ -98,16 +92,13 @@ class GeometryTestCase(BaseTestCase):
self.ema, self.ema,
] ]
for obj in objs: for obj in objs:
obj.geometry = self.geom_1 if not obj.geometry:
obj.geometry = Geometry.objects.create(geom=self.create_dummy_geometry())
obj.save() obj.save()
num_objs_with_geom += 1 data_objs = obj.geometry.get_data_objects()
geom_objs = self.geom_1.get_data_objects() self.assertEqual(len(data_objs), 1)
self.assertEqual( self.assertIn(obj, data_objs)
len(geom_objs),
num_objs_with_geom
)
self.assertIn(obj, geom_objs)
def test_as_feature_collection(self): def test_as_feature_collection(self):
geometry = self.geom_1.geom geometry = self.geom_1.geom
+5
View File
@@ -0,0 +1,5 @@
"""
Author: Michel Peltriaux
Created on: 16.07.26
"""
+5
View File
@@ -0,0 +1,5 @@
"""
Author: Michel Peltriaux
Created on: 12.09.26
"""
+353
View File
@@ -0,0 +1,353 @@
"""
Author: Michel Peltriaux
Created on: 12.09.26
"""
import json
from django.contrib.gis import gdal
from django.contrib.gis.gdal.srs import SpatialReference
from django.contrib.gis.geos import Polygon, MultiPolygon
from django.contrib.gis.geos.prototypes.io import WKTWriter
from django.utils.translation import gettext_lazy as _
from konova.settings import GEOM_MAX_VERTICES
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP, DEFAULT_SRID
class GeometryProcessor:
""" GeometryProcessor wraps methods for checking or processing geometry objects
"""
@staticmethod
def simplify_geometry(geom: MultiPolygon, max_vert: int):
""" Simplifies a geometry
Geometry will be simplified until a threshold of max vertices has been reached.
Args:
geom (MultiPolygon): The geometry
max_vert (int): Threshold of maximum vertices in geometry
Returns:
geom (MultiPolygon): The simplified geometry
"""
tolerance = 0.1
n = geom.num_coords
while(n > max_vert):
geom = geom.simplify(tolerance)
n = geom.num_coords
tolerance += 0.1
return geom
@staticmethod
def is_vertices_num_valid(geometry: MultiPolygon):
""" Checks whether the number of vertices in the geometry is not too high
Returns:
"""
return geometry.num_coords <= GEOM_MAX_VERTICES
@staticmethod
def is_valid_4326(geometry: MultiPolygon) -> bool:
""" Checks whether a given geometry's coordinates are in a valid range to be of EPSG:4326
Args:
geometry: The geometry
Returns:
ret_val (bool): Whether the geometry is valid EPSG:4326
"""
assert geometry.srid == DEFAULT_SRID
return GeometryProcessor.is_valid_coord_range(
geometry,
-90.0,
90.0,
-180.0,
180.0,
)
@staticmethod
def is_valid_25832(geometry: MultiPolygon) -> bool:
""" Checks whether a given geometry's coordinates are in a valid range to be of EPSG:25832
Args:
geometry: The geometry
Returns:
ret_val (bool): Whether the geometry is valid EPSG:25832
"""
assert geometry.srid == DEFAULT_SRID_RLP
return GeometryProcessor.is_valid_coord_range(
geometry,
166_000,
834_000,
0,
9_329_000,
)
@staticmethod
def is_valid_coord_range(geometry: MultiPolygon,
min_x:float|int,
max_x:float|int,
min_y:float|int,
max_y: float|int) -> bool:
""" Checks whether a given geometry's coordinates are in a valid range defined by input parameters
Args:
geometry: The geometry
min_x (float|int): The min x value
max_x (float|int): The max x value
min_y (float|int): The min y value
max_y (float|int): The max y value
Returns:
ret_val (bool): Whether the geometry's coordinates stays inside the defined range
"""
if not geometry.centroid.coords:
# No coordinates at all found, therefore technically proper 4326
return True
try:
env_min_x = geometry.envelope.min_x
env_max_x = geometry.envelope.max_x
env_min_y = geometry.envelope.min_y
env_max_y = geometry.envelope.max_x
valid = (
min_x <= env_min_x and
env_max_x <= max_x and
min_y <= env_min_y and
env_max_y <= max_y
)
return valid
except IndexError:
return False
@staticmethod
def cast_to_multipolygon(input_geom: MultiPolygon) -> MultiPolygon:
""" 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: gdal.OGRGeometry) -> gdal.OGRGeometry:
""" 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
@staticmethod
def flatten_geom_to_2D(geom: gdal.OGRGeometry) -> gdal.OGRGeometry:
"""
Enforces a given OGRGeometry from higher dimensions into 2D
"""
wkt_w = WKTWriter(dim=2)
g_wkt = wkt_w.write(geom.geos).decode("utf-8")
geom = gdal.OGRGeometry(g_wkt)
return geom
@staticmethod
def is_area_valid(geom: gdal.OGRGeometry) -> bool:
""" Checks whether the area is at least > 1m²
Returns:
"""
assert geom.srid == DEFAULT_SRID_RLP
is_area_valid = geom.area > 1 # > 1m² (SRID:25832)
return is_area_valid
class GeoJsonValidator:
""" GeoJson Validator validates geojson (e.g. from API or form input)
"""
_errors: list
_input_geojson: dict
_validated_geometry: MultiPolygon
_num_ignored_geometries: int
_accepted_ogr_types = [
"Polygon",
"Polygon25D",
"MultiPolygon",
"MultiPolygon25D",
]
def __init__(self, input_geojson: dict, srs: int) -> None:
assert input_geojson is not None
self._input_geojson = input_geojson
self._num_ignored_geometries = 0
self._errors = list()
self._srs = SpatialReference(srs)
@property
def num_ignored_geometries(self):
""" Getter
Returns:
"""
return self._num_ignored_geometries
@property
def input_geojson(self):
""" Getter
Returns:
"""
return self._input_geojson
@property
def validated_geometry(self):
""" Getter
Returns:
"""
return self._validated_geometry
@property
def errors(self):
""" Getter
Returns:
"""
return self._errors
def __add_error(self, text: str):
""" Wraps pushing new error into error list
Args:
text (str): The error text
Returns:
"""
self._errors.append(text)
def validate(self):
""" Validates input geojson
Returns:
"""
features = self._input_geojson.get("features", None)
is_input_geojson_empty = len(self._input_geojson) == 0
no_features_in_input_found = features is None
if not is_input_geojson_empty and no_features_in_input_found:
# check if _input_geojson is a feature itself
if self.__is_geojson_feature():
features = [
{
"geometry": self._input_geojson
}
]
else:
self.__add_error("Input does not seem to be geojson")
return
try:
validated_features = self.__validate_single_features(features)
except AssertionError as e:
self.__add_error(str(e))
return
# Unionize all polygon features into one new MultiPolygon
if validated_features:
multipolygon_geometry = MultiPolygon(*validated_features, srid=self._srs).unary_union
else:
# If no features have been processed, this indicates an empty geometry - so we store an empty geometry
multipolygon_geometry = MultiPolygon(srid=self._srs)
# Make sure to convert into a MultiPolygon. Relevant if a single Polygon is provided.
multipolygon_geometry = GeometryProcessor.cast_to_multipolygon(multipolygon_geometry)
self._validated_geometry = multipolygon_geometry
def __validate_single_features(self, features: list) -> list:
validated_features = []
# Check validity for each feature of the geometry
for feature in features:
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=self._srs)
if g.empty:
continue
g = GeometryProcessor.cast_to_rlp_srid(g)
if not GeometryProcessor.is_valid_25832(g):
raise AssertionError(_("This feature does not hold valid EPSG:25832 coordinates:\n {}".format(g.geojson)))
geometry_has_unwanted_dimensions = g.coord_dim > 2
if geometry_has_unwanted_dimensions:
g = GeometryProcessor.flatten_geom_to_2D(g)
geometry_type_is_accepted = g.geom_type not in self._accepted_ogr_types
if geometry_type_is_accepted:
raise AssertionError(_("Only surfaces allowed. Points or lines must be buffered."))
is_area_valid = GeometryProcessor.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_ignored_geometries += 1
continue
# Whatever this geometry object is -> try to create a Polygon from it
# The resulting polygon object automatically detects whether a valid polygon has been created or not
g = Polygon.from_ewkt(g.ewkt)
if not g.valid:
raise AssertionError(g.valid_reason)
# If the resulting polygon is just a single polygon, we add it to the list of properly created features
if isinstance(g, Polygon):
validated_features.append(g)
elif isinstance(g, MultiPolygon):
# The resulting polygon could be of type MultiPolygon (due to multiple surfaces)
# If so, we extract each single polygons from the MultiPolygon and add them to the created features list
validated_features.extend(list(g))
return validated_features
def __is_geojson_feature(self):
""" Checks whether _input_geojson is a proper geojson feature
Returns:
"""
has_type = self._input_geojson.get("type", None) is not None
has_coordinates = self._input_geojson.get("coordinates", None) is not None
has_properties = self._input_geojson.get("properties", None) is not None
return has_type and has_coordinates and has_properties
+5
View File
@@ -0,0 +1,5 @@
"""
Author: Michel Peltriaux
Created on: 16.07.26
"""
+4
View File
@@ -9,6 +9,7 @@ import json
from json import JSONDecodeError from json import JSONDecodeError
import requests import requests
from django.contrib.gis import gdal
from konova.sub_settings import schneider_settings from konova.sub_settings import schneider_settings
from konova.sub_settings.lanis_settings import DEFAULT_SRID from konova.sub_settings.lanis_settings import DEFAULT_SRID
@@ -35,7 +36,10 @@ class ParcelFetcher:
if geom.area < buffer_threshold: if geom.area < buffer_threshold:
# Fallback for malicious geometries which are way too small and would disappear on negative buffering # Fallback for malicious geometries which are way too small and would disappear on negative buffering
geom = geometry.geom geom = geometry.geom
try:
geom.transform(DEFAULT_SRID) geom.transform(DEFAULT_SRID)
except gdal.GDALException as e:
raise ValueError(f"{str(e)}\nCould not transform geometry (id: {geometry.id}) to {DEFAULT_SRID}:\n {geom.geojson}")
self.geojson = geom.ewkt self.geojson = geom.ewkt
self.results = [] self.results = []
Binary file not shown.
+67 -48
View File
@@ -30,7 +30,7 @@
#: konova/filters/mixins/office.py:57 konova/filters/mixins/record.py:23 #: 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/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/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/geometry_form.py:31 konova/forms/modals/document_form.py:26
#: konova/forms/modals/document_form.py:36 #: konova/forms/modals/document_form.py:36
#: konova/forms/modals/document_form.py:50 #: konova/forms/modals/document_form.py:50
#: konova/forms/modals/document_form.py:62 #: konova/forms/modals/document_form.py:62
@@ -45,7 +45,7 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: PACKAGE VERSION\n" "Project-Id-Version: PACKAGE VERSION\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-12-14 17:23+0100\n" "POT-Creation-Date: 2026-09-13 12:06+0200\n"
"PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n" "PO-Revision-Date: YEAR-MO-DA HO:MI+ZONE\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n" "Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n" "Language-Team: LANGUAGE <LL@li.org>\n"
@@ -129,6 +129,7 @@ msgstr ""
#: analysis/templates/analysis/reports/includes/old_data/amount.html:3 #: analysis/templates/analysis/reports/includes/old_data/amount.html:3
#: compensation/forms/modals/compensation_action.py:66 #: compensation/forms/modals/compensation_action.py:66
#: compensation/templates/compensation/detail/eco_account/includes/deductions.html:34 #: compensation/templates/compensation/detail/eco_account/includes/deductions.html:34
#: compensation/templates/compensation/report/eco_account/report.html:50
#: intervention/templates/intervention/detail/includes/deductions.html:31 #: intervention/templates/intervention/detail/includes/deductions.html:31
msgid "Amount" msgid "Amount"
msgstr "Menge" msgstr "Menge"
@@ -334,6 +335,7 @@ msgstr "Typ"
#: analysis/templates/analysis/reports/includes/old_data/amount.html:24 #: analysis/templates/analysis/reports/includes/old_data/amount.html:24
#: compensation/tables/compensation.py:87 #: compensation/tables/compensation.py:87
#: compensation/templates/compensation/report/eco_account/report.html:47
#: intervention/forms/modals/deduction.py:58 #: intervention/forms/modals/deduction.py:58
#: intervention/forms/modals/deduction.py:65 intervention/tables.py:92 #: intervention/forms/modals/deduction.py:65 intervention/tables.py:92
#: intervention/templates/intervention/detail/view.html:19 #: intervention/templates/intervention/detail/view.html:19
@@ -448,7 +450,8 @@ msgid "Select the intervention for which this compensation compensates"
msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist" msgstr "Wählen Sie den Eingriff, für den diese Kompensation bestimmt ist"
#: compensation/forms/compensation.py:114 #: compensation/forms/compensation.py:114
#: compensation/views/compensation/compensation.py:121 #: compensation/views/compensation/compensation.py:96
#: compensation/views/compensation/compensation.py:160
msgid "New compensation" msgid "New compensation"
msgstr "Neue Kompensation" msgstr "Neue Kompensation"
@@ -475,7 +478,8 @@ msgid "When did the parties agree on this?"
msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?" msgstr "Wann wurde dieses Ökokonto offiziell vereinbart?"
#: compensation/forms/eco_account.py:73 #: compensation/forms/eco_account.py:73
#: compensation/views/eco_account/eco_account.py:105 #: compensation/views/eco_account/eco_account.py:77
#: compensation/views/eco_account/eco_account.py:127
msgid "New Eco-Account" msgid "New Eco-Account"
msgstr "Neues Ökokonto" msgstr "Neues Ökokonto"
@@ -503,7 +507,8 @@ msgstr ""
#: compensation/forms/eco_account.py:257 #: compensation/forms/eco_account.py:257
msgid "Please contact the responsible conservation office to find a solution!" msgid "Please contact the responsible conservation office to find a solution!"
msgstr "Kontaktieren Sie die zuständige Naturschutzbehörde um eine Lösung zu finden!" msgstr ""
"Kontaktieren Sie die zuständige Naturschutzbehörde um eine Lösung zu finden!"
#: compensation/forms/mixins.py:37 #: compensation/forms/mixins.py:37
#: compensation/templates/compensation/detail/eco_account/view.html:63 #: compensation/templates/compensation/detail/eco_account/view.html:63
@@ -818,6 +823,7 @@ msgstr "Am {} von {} verzeichnet worden"
#: compensation/tables/eco_account.py:39 #: compensation/tables/eco_account.py:39
#: compensation/templates/compensation/detail/eco_account/view.html:36 #: compensation/templates/compensation/detail/eco_account/view.html:36
#: compensation/templates/compensation/report/eco_account/report.html:34
#: konova/templates/konova/widgets/progressbar.html:3 #: konova/templates/konova/widgets/progressbar.html:3
msgid "Available" msgid "Available"
msgstr "Verfügbar" msgstr "Verfügbar"
@@ -948,7 +954,7 @@ msgstr "Wiedervorlage"
#: compensation/templates/compensation/detail/compensation/includes/controls.html:20 #: compensation/templates/compensation/detail/compensation/includes/controls.html:20
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:34 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:34
#: ema/templates/ema/detail/includes/controls.html:34 #: ema/templates/ema/detail/includes/controls.html:32
#: intervention/templates/intervention/detail/includes/controls.html:39 #: intervention/templates/intervention/detail/includes/controls.html:39
msgid "Edit" msgid "Edit"
msgstr "Bearbeiten" msgstr "Bearbeiten"
@@ -962,7 +968,7 @@ msgstr "Log anzeigen"
#: compensation/templates/compensation/detail/compensation/includes/controls.html:27 #: compensation/templates/compensation/detail/compensation/includes/controls.html:27
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:41 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:41
#: ema/templates/ema/detail/includes/controls.html:41 #: ema/templates/ema/detail/includes/controls.html:43
#: intervention/templates/intervention/detail/includes/controls.html:46 #: intervention/templates/intervention/detail/includes/controls.html:46
msgid "Delete" msgid "Delete"
msgstr "Löschen" msgstr "Löschen"
@@ -1154,7 +1160,7 @@ msgstr "Verzeichnet am"
#: compensation/templates/compensation/detail/compensation/view.html:107 #: compensation/templates/compensation/detail/compensation/view.html:107
#: compensation/templates/compensation/detail/eco_account/view.html:85 #: compensation/templates/compensation/detail/eco_account/view.html:85
#: compensation/templates/compensation/report/compensation/report.html:54 #: compensation/templates/compensation/report/compensation/report.html:54
#: compensation/templates/compensation/report/eco_account/report.html:47 #: compensation/templates/compensation/report/eco_account/report.html:75
#: ema/templates/ema/detail/view.html:71 #: ema/templates/ema/detail/view.html:71
#: ema/templates/ema/report/report.html:34 #: ema/templates/ema/report/report.html:34
#: intervention/templates/intervention/detail/view.html:113 #: intervention/templates/intervention/detail/view.html:113
@@ -1188,7 +1194,7 @@ msgid "other users"
msgstr "weitere Nutzer" msgstr "weitere Nutzer"
#: compensation/templates/compensation/detail/eco_account/includes/controls.html:18 #: compensation/templates/compensation/detail/eco_account/includes/controls.html:18
#: ema/templates/ema/detail/includes/controls.html:18 #: ema/templates/ema/detail/includes/controls.html:19
#: intervention/forms/modals/share.py:62 #: intervention/forms/modals/share.py:62
#: intervention/templates/intervention/detail/includes/controls.html:18 #: intervention/templates/intervention/detail/includes/controls.html:18
#: intervention/tests/unit/test_forms.py:150 #: intervention/tests/unit/test_forms.py:150
@@ -1258,11 +1264,11 @@ msgstr "Maßnahmenträger"
msgid "Report" msgid "Report"
msgstr "Bericht" msgstr "Bericht"
#: compensation/templates/compensation/report/eco_account/report.html:34 #: compensation/templates/compensation/report/eco_account/report.html:43
msgid "Deductions for" msgid "Deductions for"
msgstr "Abbuchungen für" msgstr "Abbuchungen für"
#: compensation/templates/compensation/report/eco_account/report.html:42 #: compensation/templates/compensation/report/eco_account/report.html:64
#: intervention/templates/intervention/report/report.html:53 #: intervention/templates/intervention/report/report.html:53
#: intervention/templates/intervention/report/report.html:66 #: intervention/templates/intervention/report/report.html:66
#: intervention/templates/intervention/report/report.html:76 #: intervention/templates/intervention/report/report.html:76
@@ -1292,40 +1298,44 @@ msgstr ""
msgid "Responsible data" msgid "Responsible data"
msgstr "Daten zu den verantwortlichen Stellen" msgstr "Daten zu den verantwortlichen Stellen"
#: compensation/views/compensation/compensation.py:52 #: compensation/views/compensation/compensation.py:53
msgid "Compensations - Overview" msgid "Compensations - Overview"
msgstr "Kompensationen - Übersicht" msgstr "Kompensationen - Übersicht"
#: compensation/views/compensation/compensation.py:167 #: compensation/views/compensation/compensation.py:203
#: compensation/views/compensation/compensation.py:261
#: compensation/views/eco_account/eco_account.py:169
#: compensation/views/eco_account/eco_account.py:224 ema/views/ema.py:175
#: ema/views/ema.py:229 intervention/views/intervention.py:176
#: intervention/views/intervention.py:230
msgid "Edit {}"
msgstr "Bearbeite {}"
#: compensation/views/compensation/compensation.py:242
#: konova/utils/message_templates.py:40 #: konova/utils/message_templates.py:40
msgid "Compensation {} edited" msgid "Compensation {} edited"
msgstr "Kompensation {} bearbeitet" msgstr "Kompensation {} bearbeitet"
#: compensation/views/compensation/compensation.py:190
#: compensation/views/eco_account/eco_account.py:168 ema/views/ema.py:173
#: intervention/views/intervention.py:175
msgid "Edit {}"
msgstr "Bearbeite {}"
#: compensation/views/compensation/report.py:35 #: compensation/views/compensation/report.py:35
#: compensation/views/eco_account/report.py:35 ema/views/report.py:35 #: compensation/views/eco_account/report.py:35 ema/views/report.py:35
#: intervention/views/report.py:36 #: intervention/views/report.py:36
msgid "Report {}" msgid "Report {}"
msgstr "Bericht {}" msgstr "Bericht {}"
#: compensation/views/eco_account/eco_account.py:49 #: compensation/views/eco_account/eco_account.py:51
msgid "Eco-account - Overview" msgid "Eco-account - Overview"
msgstr "Ökokonten - Übersicht" msgstr "Ökokonten - Übersicht"
#: compensation/views/eco_account/eco_account.py:82 #: compensation/views/eco_account/eco_account.py:108
msgid "Eco-Account {} added" msgid "Eco-Account {} added"
msgstr "Ökokonto {} hinzugefügt" msgstr "Ökokonto {} hinzugefügt"
#: compensation/views/eco_account/eco_account.py:145 #: compensation/views/eco_account/eco_account.py:205
msgid "Eco-Account {} edited" msgid "Eco-Account {} edited"
msgstr "Ökokonto {} bearbeitet" msgstr "Ökokonto {} bearbeitet"
#: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:107 #: ema/forms.py:42 ema/tests/unit/test_forms.py:27 ema/views/ema.py:79
#: ema/views/ema.py:129
msgid "New EMA" msgid "New EMA"
msgstr "Neue EMA hinzufügen" msgstr "Neue EMA hinzufügen"
@@ -1357,11 +1367,11 @@ msgstr "Ersatzzahlungsmaßnahme"
msgid "EMAs - Overview" msgid "EMAs - Overview"
msgstr "EMAs - Übersicht" msgstr "EMAs - Übersicht"
#: ema/views/ema.py:85 #: ema/views/ema.py:111
msgid "EMA {} added" msgid "EMA {} added"
msgstr "EMA {} hinzugefügt" msgstr "EMA {} hinzugefügt"
#: ema/views/ema.py:150 #: ema/views/ema.py:211
msgid "EMA {} edited" msgid "EMA {} edited"
msgstr "EMA {} bearbeitet" msgstr "EMA {} bearbeitet"
@@ -1425,7 +1435,7 @@ msgstr "Datum Bestandskraft bzw. Rechtskraft"
#: intervention/forms/intervention.py:216 #: intervention/forms/intervention.py:216
#: intervention/tests/unit/test_forms.py:36 #: intervention/tests/unit/test_forms.py:36
#: intervention/views/intervention.py:109 #: intervention/views/intervention.py:82 intervention/views/intervention.py:134
msgid "New intervention" msgid "New intervention"
msgstr "Neuer Eingriff" msgstr "Neuer Eingriff"
@@ -1661,15 +1671,15 @@ msgstr ""
msgid "Check performed" msgid "Check performed"
msgstr "Prüfung durchgeführt" msgstr "Prüfung durchgeführt"
#: intervention/views/intervention.py:53 #: intervention/views/intervention.py:56
msgid "Interventions - Overview" msgid "Interventions - Overview"
msgstr "Eingriffe - Übersicht" msgstr "Eingriffe - Übersicht"
#: intervention/views/intervention.py:86 #: intervention/views/intervention.py:114
msgid "Intervention {} added" msgid "Intervention {} added"
msgstr "Eingriff {} hinzugefügt" msgstr "Eingriff {} hinzugefügt"
#: intervention/views/intervention.py:150 #: intervention/views/intervention.py:212
msgid "Intervention {} edited" msgid "Intervention {} edited"
msgstr "Eingriff {} bearbeitet" msgstr "Eingriff {} bearbeitet"
@@ -1797,16 +1807,11 @@ msgstr "Speichern"
msgid "Not editable" msgid "Not editable"
msgstr "Nicht editierbar" msgstr "Nicht editierbar"
#: konova/forms/geometry_form.py:31 konova/utils/quality.py:44 #: konova/forms/geometry_form.py:30 konova/utils/quality.py:44
#: konova/utils/quality.py:46 templates/form/collapsable/form.html:45 #: konova/utils/quality.py:46 templates/form/collapsable/form.html:45
msgid "Geometry" msgid "Geometry"
msgstr "Geometrie" msgstr "Geometrie"
#: konova/forms/geometry_form.py:105
msgid "Only surfaces allowed. Points or lines must be buffered."
msgstr ""
"Nur Flächen erlaubt. Punkte oder Linien müssen zu Flächen gepuffert werden."
#: konova/forms/modals/document_form.py:37 #: konova/forms/modals/document_form.py:37
msgid "When has this file been created? Important for photos." msgid "When has this file been created? Important for photos."
msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?" msgstr "Wann wurde diese Datei erstellt oder das Foto aufgenommen?"
@@ -1961,6 +1966,7 @@ msgstr "Raumreferenz"
#: konova/templates/konova/includes/parcels/parcels.html:28 #: konova/templates/konova/includes/parcels/parcels.html:28
msgid "No geometry entry found on database. Please contact an admin!" msgid "No geometry entry found on database. Please contact an admin!"
msgstr "" msgstr ""
"Keine Geometrie in Datenbank gefunden. Kontaktieren Sie einen Administrator!"
#: konova/templates/konova/includes/quickstart/compensations.html:20 #: konova/templates/konova/includes/quickstart/compensations.html:20
#: konova/templates/konova/includes/quickstart/ecoaccounts.html:20 #: konova/templates/konova/includes/quickstart/ecoaccounts.html:20
@@ -2006,39 +2012,52 @@ msgstr "In Zwischenablage kopiert"
msgid "Search" msgid "Search"
msgstr "Suchen" msgstr "Suchen"
#: konova/utils/mailer.py:69 konova/utils/mailer.py:146 #: konova/utils/geometry/geometry_validator.py:311
msgid ""
"This feature does not hold valid EPSG:25832 coordinates:\n"
" {}"
msgstr ""
"Dieses Feature enthält keine validen EPSG:25832 Koordinaten:\n"
" {}"
#: konova/utils/geometry/geometry_validator.py:319
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/utils/mailer.py:82 konova/utils/mailer.py:159
msgid "{} - Shared access removed" msgid "{} - Shared access removed"
msgstr "{} - Zugriff entzogen" msgstr "{} - Zugriff entzogen"
#: konova/utils/mailer.py:94 konova/utils/mailer.py:120 #: konova/utils/mailer.py:107 konova/utils/mailer.py:133
msgid "{} - Shared access given" msgid "{} - Shared access given"
msgstr "{} - Zugriff freigegeben" msgstr "{} - Zugriff freigegeben"
#: konova/utils/mailer.py:172 konova/utils/mailer.py:325 #: konova/utils/mailer.py:185 konova/utils/mailer.py:338
msgid "{} - Shared data unrecorded" msgid "{} - Shared data unrecorded"
msgstr "{} - Freigegebene Daten entzeichnet" msgstr "{} - Freigegebene Daten entzeichnet"
#: konova/utils/mailer.py:198 konova/utils/mailer.py:300 #: konova/utils/mailer.py:211 konova/utils/mailer.py:313
msgid "{} - Shared data recorded" msgid "{} - Shared data recorded"
msgstr "{} - Freigegebene Daten verzeichnet" msgstr "{} - Freigegebene Daten verzeichnet"
#: konova/utils/mailer.py:224 konova/utils/mailer.py:375 #: konova/utils/mailer.py:237 konova/utils/mailer.py:388
msgid "{} - Shared data checked" msgid "{} - Shared data checked"
msgstr "{} - Freigegebene Daten geprüft" msgstr "{} - Freigegebene Daten geprüft"
#: konova/utils/mailer.py:249 konova/utils/mailer.py:401 #: konova/utils/mailer.py:262 konova/utils/mailer.py:414
msgid "{} - Deduction changed" msgid "{} - Deduction changed"
msgstr "{} - Abbuchung geändert" msgstr "{} - Abbuchung geändert"
#: konova/utils/mailer.py:275 konova/utils/mailer.py:350 #: konova/utils/mailer.py:288 konova/utils/mailer.py:363
msgid "{} - Shared data deleted" msgid "{} - Shared data deleted"
msgstr "{} - Freigegebene Daten gelöscht" msgstr "{} - Freigegebene Daten gelöscht"
#: konova/utils/mailer.py:422 templates/email/api/verify_token.html:4 #: konova/utils/mailer.py:435 templates/email/api/verify_token.html:4
msgid "Request for new API token" msgid "Request for new API token"
msgstr "Anfrage für neuen API Token" msgstr "Anfrage für neuen API Token"
#: konova/utils/mailer.py:447 #: konova/utils/mailer.py:460
msgid "Resubmission - {}" msgid "Resubmission - {}"
msgstr "Wiedervorlage - {}" msgstr "Wiedervorlage - {}"
@@ -2289,11 +2308,11 @@ msgstr "Neuer Token generiert. Administratoren sind informiert."
msgid "missing" msgid "missing"
msgstr "fehlend" msgstr "fehlend"
#: konova/utils/tables.py:222 #: konova/utils/tables.py:224
msgid "Full access granted" msgid "Full access granted"
msgstr "Für Sie freigegeben - Datensatz kann bearbeitet werden" msgstr "Für Sie freigegeben - Datensatz kann bearbeitet werden"
#: konova/utils/tables.py:222 #: konova/utils/tables.py:224
msgid "Access not granted" msgid "Access not granted"
msgstr "Nicht freigegeben - Datensatz nur lesbar" msgstr "Nicht freigegeben - Datensatz nur lesbar"
@@ -2303,7 +2322,7 @@ msgstr ""
"Dieses Datum ist unrealistisch. Geben Sie bitte das korrekte Datum ein " "Dieses Datum ist unrealistisch. Geben Sie bitte das korrekte Datum ein "
"(>1950)." "(>1950)."
#: konova/views/home.py:75 templates/navbars/navbar.html:16 #: konova/views/home.py:76 templates/navbars/navbar.html:16
msgid "Home" msgid "Home"
msgstr "Home" msgstr "Home"
@@ -2323,7 +2342,7 @@ msgstr "{} verzeichnet"
msgid "Errors found:" msgid "Errors found:"
msgstr "Fehler gefunden:" msgstr "Fehler gefunden:"
#: konova/views/remove.py:35 #: konova/views/remove.py:34
msgid "{} removed" msgid "{} removed"
msgstr "{} entfernt" msgstr "{} entfernt"
+25
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;
}
+38 -38
View File
@@ -1,65 +1,65 @@
amqp==5.3.1 amqp==5.3.1
asgiref==3.11.1 asgiref==3.12.1
async-timeout==5.0.1 async-timeout==5.0.1
beautifulsoup4==4.14.3 beautifulsoup4==4.15.0
billiard==4.2.4 billiard==4.2.4
cached-property==2.0.1 cached-property==2.0.1
celery==5.6.3 celery==5.6.3
certifi==2026.4.22 certifi==2026.7.22
cffi==2.0.0 cffi==2.1.1
chardet==7.4.3 chardet==7.6.0
charset-normalizer==3.4.7 charset-normalizer==3.5.1
click==8.3.3 click==8.5.0
click-didyoumean==0.3.1 click-didyoumean==0.3.1
click-plugins==1.1.1.2 click-plugins==1.1.1.2
click-repl==0.3.0 click-repl==0.3.0
coverage==7.13.5 coverage==7.16.0
cryptography==48.0.0 cryptography==50.0.1
Deprecated==1.3.1 Deprecated==1.3.1
Django==6.0.5 Django==6.1.1
django-autocomplete-light==4.0.0 django-autocomplete-light==5.0.0
django-bootstrap-modal-forms==3.0.5 django-bootstrap-modal-forms==3.0.5
django-bootstrap4==26.1 django-bootstrap4==26.2
django-environ==0.13.0 django-environ==0.14.0
django-filter==25.2 django-filter==26.1
django-fontawesome-5==1.0.18 django-fontawesome-5==1.0.18
django-oauth-toolkit==3.2.0 django-oauth-toolkit==3.4.1
django-tables2==3.0.0 django-tables2==3.0.1
et_xmlfile==2.0.0 et_xmlfile==2.0.0
gunicorn==26.0.0 gunicorn==26.2.0
idna==3.13 idna==3.19
importlib_metadata==9.0.0 importlib_metadata==9.0.1
itsdangerous==2.2.0 itsdangerous==2.2.0
jwcrypto==1.5.7 jwcrypto==1.6.0
kombu==5.6.2 kombu==5.6.2
oauthlib==3.3.1 oauthlib==3.3.1
openpyxl==3.2.0b1 openpyxl==3.2.0b1
packaging==26.2 packaging==26.3
pika==1.4.0 pika==1.4.4
pillow==12.2.0 pillow==12.3.0
prompt_toolkit==3.0.52 prompt_toolkit==3.0.53
psycopg==3.3.4 psycopg==3.3.5
psycopg-binary==3.3.4 psycopg-binary==3.3.5
pycparser==3.0 pycparser==3.0
pyparsing==3.3.2 pyparsing==3.3.2
pypng==0.20220715.0 pypng==0.20220715.0
pyproj==3.7.2 pyproj==3.8.0
python-dateutil==2.9.0.post0 python-dateutil==2.9.0.post0
pytz==2026.2 pytz==2026.3
PyYAML==6.0.3 PyYAML==6.0.3
qrcode==8.2 qrcode==8.2
redis==7.4.0 redis==8.1.0
requests==2.33.1 requests==2.34.2
six==1.17.0 six==1.17.0
soupsieve==2.8.3 soupsieve==2.9.2
sqlparse==0.5.5 sqlparse==0.6.0
typing_extensions==4.15.0 typing_extensions==4.16.0
tzdata==2026.2 tzdata==2026.4
tzlocal==5.3.1 tzlocal==5.4.4
urllib3==2.7.0 urllib3==2.7.0
vine==5.1.0 vine==5.1.0
wcwidth==0.7.0 wcwidth==0.8.3
webservices==0.7 webservices==0.7
wrapt==2.1.2 wrapt==2.4.1
xmltodict==1.0.4 xmltodict==1.0.4
zipp==3.23.1 zipp==4.1.0
+1 -3
View File
@@ -56,11 +56,9 @@
], ],
"layers": "layers":
[ [
{ "id": "webatlas_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "WebatlasRP farbig", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_WebAtlasRP/MapServer/WmsServer?", "name": "RP_WebAtlasRP", "active": true},
{ "id": "webatlas_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "WebatlasRP grau", "attribution": "LVermGeo", "url": "https://maps.service24.rlp.de/gisserver/services/RP/RP_ETRS_Gt/MapServer/WmsServer?", "name": "0", "active": false },
{ "id": "luftbilder", "folder": "bg", "type": "WMS", "order": -1, "title": "Luftbilder", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/rp_dop20.fcgi?", "name": "rp_dop20", "active": false }, { "id": "luftbilder", "folder": "bg", "type": "WMS", "order": -1, "title": "Luftbilder", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/rp_dop20.fcgi?", "name": "rp_dop20", "active": false },
{ "id": "basemap_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "BasemapDE farbig", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_farbe", "active": false }, { "id": "basemap_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "BasemapDE farbig", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_farbe", "active": false },
{ "id": "basemap_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "BasemapDE grau", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_grau", "active": false }, { "id": "basemap_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "BasemapDE grau", "attribution": "BKG", "url": "https://sgx.geodatenzentrum.de/wms_basemapde?", "name": "de_basemapde_web_raster_grau", "active": true },
{ "id": "dtk_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "DTK5 farbig", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/dtk5_rp.fcgi?", "name": "rp_dtk5", "active": false }, { "id": "dtk_farbe", "folder": "bg", "type": "WMS", "order": -1, "title": "DTK5 farbig", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/dtk5_rp.fcgi?", "name": "rp_dtk5", "active": false },
{ "id": "dtk_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "DTK5 grau", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/dtk5_rp.fcgi?", "name": "rp_dtk5_grau", "active": false }, { "id": "dtk_grau", "folder": "bg", "type": "WMS", "order": -1, "title": "DTK5 grau", "attribution": "LVermGeo", "url": "https://geo4.service24.rlp.de/wms/dtk5_rp.fcgi?", "name": "rp_dtk5_grau", "active": false },