Compare commits

...

112 Commits
v1.2 ... master

Author SHA1 Message Date
faf8aed777 Merge pull request '# Drop django-simple-sso' (#468) from 467_Remove_django-simple-sso into master
Reviewed-on: #468
2025-01-24 16:12:06 +01:00
94c498866f # Drop django-simple-sso
* drops django-simple-sso package from project
* drops unused messenger.py
2025-01-24 16:11:23 +01:00
616965c890 Merge pull request 'bugfix' (#465) from bugfix into master
Reviewed-on: #465
2025-01-21 13:43:46 +01:00
e39c7eb51f # KSP Token optimization
* adds support for standardized bearer token usage instead of ksptoken/kspuser header usage (still supported)
2025-01-21 13:38:37 +01:00
19bd408fbd # Bugfix code update
* fixes bug where empty short names were not resolved properly
2025-01-21 12:52:46 +01:00
7bfe6a37f8 Merge pull request '# 456 Rework API key creation' (#462) from 456_Rework_API_key_creation into master
Reviewed-on: #462
2025-01-08 16:04:12 +01:00
9b63307f01 # 456 Rework API key creation
* removes frontend input field holding generated API key
* replaces with modal form
* reworks tests on API token form
2025-01-08 16:03:26 +01:00
123a470006 Merge pull request '# 457 User autocomplete fix' (#461) from 457_User_autocomplete_fix into master
Reviewed-on: #461
2025-01-08 14:36:01 +01:00
5a0c5285e7 # 457 User autocomplete fix
* fixes bug where empty query parameter would show users in autocomplete share view
* fixes same behaviour on autocomplete share team view
2025-01-08 14:35:35 +01:00
3a01eaff92 Merge pull request '# 450 Optimization recalculate_parcels command' (#460) from 450_Optimierung_Kommando_Nachverschneidung into master
Reviewed-on: #460
2025-01-08 14:27:55 +01:00
2af91aa178 # 450 Optimization recalculate_parcels command
* optimizes recalculate_parcels.py command so that only non-empty geometries will be processed
* drops test_identifier_generating.py command due to missing usage
2025-01-08 14:27:23 +01:00
53d0af89ac Merge pull request '# Hotfix' (#458) from oauth_fix into master
Reviewed-on: #458
2024-12-23 13:41:53 +01:00
7b5c1f0d97 # Hotfix
* fixes bug where anonymous user trying to logout would throw error
2024-12-23 13:41:25 +01:00
ef076c0b3b Merge pull request 'oauth_fix' (#453) from oauth_fix into master
Reviewed-on: #453
2024-12-23 12:09:20 +01:00
72a5075f3b # Update dependencies
* updates requirements.txt
2024-12-23 12:03:15 +01:00
d677ac6b5a # Map proxy enhancement
* adds whitelisting for map proxy hosts
2024-12-23 11:08:41 +01:00
9149e4cbd3 # Propagation improvement
* fixes documentation and variable names on oauth token revocation
* introduces private key for propagation
* changes key usage in decryption of propagated user data from oauth_client_id to private propagation key
2024-12-23 10:45:08 +01:00
1c24cbea26 # OAuth Token revocation
* adds revocation of user tokens on logout
2024-12-23 09:26:14 +01:00
fa89bbba99 Merge pull request '# Bugfix: Recalculate_parcels command' (#448) from bugfixing into master
Reviewed-on: #448
2024-11-13 16:09:22 +01:00
78eb711057 # Bugfix: Recalculate_parcels command
* fixes a bug on recalculate_parcels if not --force-all is used
2024-11-13 16:08:36 +01:00
416ad8478c Merge pull request '439_Wartungskommando_Nachverschneidung' (#446) from 439_Wartungskommando_Nachverschneidung into master
Reviewed-on: #446
2024-10-26 10:24:50 +02:00
6b28c4ec15 # Drop atomic transaction
* drops atomic transaction processing on Parcel.make_unique
2024-10-26 10:24:10 +02:00
46a2a4ff46 # Parcel recalculation optimization
* enhances workflow for parcel recalculation
2024-10-26 10:17:09 +02:00
90e5cf5b36 Merge pull request '# Parcel duplicate repair' (#444) from 439_Wartungskommando_Nachverschneidung into master
Reviewed-on: #444
2024-10-26 09:48:32 +02:00
50f46e319c # Parcel duplicate repair
* adds mechanic to repair parcels in case of unwanted parcel duplicates
* optimizes filtering of geometries for parcel recalculation
2024-10-26 09:47:27 +02:00
e2ea087c4e Merge pull request '# Wartungskommando Optimization' (#442) from 439_Wartungskommando_Nachverschneidung into master
Reviewed-on: #442
2024-10-25 19:27:02 +02:00
a6e43b044b # Wartungskommando Optimization
* extends filtering for recalculatable geometries to records without started calculation at any point (parcel_update_start is null)
* catches exceptions on geometries which could not be recalculated properly, adds them to output for further analysis
* simplifies complexity factor calculation
2024-10-25 19:19:08 +02:00
be0d261e81 Merge pull request '# 439 Wartungskommando Nachverschneidung' (#440) from 439_Wartungskommando_Nachverschneidung into master
Reviewed-on: #440
2024-10-25 14:24:18 +02:00
62e1b046c3 # 439 Wartungskommando Nachverschneidung
* refactors command update_all_parcels into recalculate_parcels
* fixes bug in command generate_report
2024-10-25 14:23:21 +02:00
669a12410f Merge pull request 'missing_migrations' (#437) from missing_migrations into master
Reviewed-on: #437
2024-08-26 18:57:10 +02:00
dd77e6c16e Merge branch 'refs/heads/master' into missing_migrations 2024-08-26 18:53:11 +02:00
33774ce557 # Migrations
* adds missing migrations
* renames variables shadowing in-builts
2024-08-26 18:51:58 +02:00
dc3dc99b3d Merge pull request '# User filtering' (#435) from 433_Filter_by_user into master
Reviewed-on: #435
2024-08-19 11:42:18 +02:00
315f9de958 # User filtering
* adds query filter to search for logged users on entries
2024-08-19 11:38:09 +02:00
0726c15086 Merge pull request '432_Unreadable_payments' (#434) from 432_Unreadable_payments into master
Reviewed-on: #434
2024-08-19 10:26:40 +02:00
2492a8abe8 # Codelist migration optimization
* adds boolean to de-/activate migration logic inside of 0002_migrate_975_to_288.py
2024-08-19 10:23:05 +02:00
dbc5cba5d7 # Variable refactoring
* renames variable `has_access` into `is_entry_shared` for better understanding in various places (mostly html related)
2024-08-19 09:44:45 +02:00
c8948ddaea # Censor payments
* censor payments if entry is not shared with user
* updates translations
2024-08-19 09:39:58 +02:00
5039da28aa Merge pull request '# Hotfix' (#430) from 427_Integration_of_codelist_288 into master
Reviewed-on: #430
2024-08-07 12:07:04 +02:00
4567339570 # Hotfix
* fixes requirements dependency
2024-08-07 12:06:27 +02:00
768849e646 Merge pull request '427_Integration_of_codelist_288' (#428) from 427_Integration_of_codelist_288 into master
Reviewed-on: #428
2024-08-07 12:01:46 +02:00
ebf10645fc # Requirements update
* updates some packages in requirements.txt
2024-08-07 09:17:20 +02:00
df241747cf # Migration list 975->288
* adds migration for app codelist to migrate existing biotope type details codes from list 975 to 288 depending on their atomID
* improves rendering of action and biotope type details on frontend for KOM, OEK and EMA
* refactors KonovaCode str() rendering
2024-08-07 09:12:38 +02:00
94e9035e10 # Codelist 288
* introduces 288 to codelist/settings.py
* refactors usage from 975 to 288
* enhances rendering of codelist names depending on which name exists (short vs long)
2024-08-06 15:39:01 +02:00
d4d39689cc Merge pull request '# Bugfix' (#425) from 424_Archived_codes_selectable into master
Reviewed-on: #425
2024-08-06 14:28:16 +02:00
2fde3f0fa3 # Bugfix
* fixes bug where archived codes has been selectable due to recursive building of child-parent hierarchy
2024-08-06 14:27:38 +02:00
b62c2e92c9 Merge pull request '# Dependency fix' (#422) from 419_Dependency_upgrade into master
Reviewed-on: #422
2024-07-10 09:30:14 +02:00
1c0fb801e6 # Dependency fix
* fixes dependency mismatch between requests 2.32.3 and kombu (requires < 2.32.0)
2024-07-10 09:29:54 +02:00
a1acff5e90 Merge pull request '# Dependency update' (#420) from 419_Dependency_upgrade into master
Reviewed-on: #420
2024-07-10 09:26:35 +02:00
25a92f59aa # Dependency update
* updates dependencies due to important version changes
2024-07-10 09:26:18 +02:00
884db6f014 Merge pull request '# Readonly map' (#417) from map_client_update_08-07-2024 into master
Reviewed-on: #417
2024-07-08 18:44:00 +02:00
59b7f3c69a # Readonly map
* fixes bug where readonly mode of map client was not readonly at all
2024-07-08 06:59:15 +02:00
0446d50438 Merge pull request '# .env fix' (#415) from env_fix into master
Reviewed-on: #415
2024-07-05 10:51:25 +02:00
12f78c85bf # .env fix
* adds celery setting to .env.sample
2024-07-05 10:50:25 +02:00
78485a4506 Merge pull request '# Requirements update' (#413) from fix_sso into master
Reviewed-on: #413
2024-07-04 11:42:05 +02:00
21a5c84b18 # Requirements update
* due to existing migrations, django-simple-sso needs to be added as a dependency as well as itsdangerous (dependency of django-simple-sso)
    * however, there is no active usage of any of these packages anymore
2024-07-04 11:39:17 +02:00
a93f509d51 Merge pull request 'env' (#411) from env into master
Reviewed-on: #411
2024-07-04 08:36:04 +02:00
38967da201 Merge pull request '407_Drop_django-simple-sso' (#410) from 407_Drop_django-simple-sso into master
Reviewed-on: #410
2024-07-04 07:58:15 +02:00
60d749db2d # Geopackage import configuration
* corrects config for geopackage import support
2024-07-04 07:44:08 +02:00
dff577309e Merge pull request '# Send-to-EGON cmd' (#408) from sending_to_egon_cmd into master
Reviewed-on: #408
2024-06-18 11:49:35 +02:00
ea590d0868 # Send-to-EGON cmd
* adds new custom command send_to_egon for performing EGON sending on a list of intervention ids
2024-06-18 11:48:56 +02:00
e09c15bd51 # Updates sso
* adds env usage for sso settings
2024-06-14 13:04:25 +02:00
c3019f83fd Merge branch 'refs/heads/407_Drop_django-simple-sso' into env
# Conflicts:
#	konova/sub_settings/sso_settings.py
#	requirements.txt
2024-06-14 13:02:33 +02:00
93a71a7055 # Requirements update
* updates requirements.txt
* drops django-simple-sso from codebase and requirements.txt
2024-06-14 13:00:09 +02:00
35b1409359 # Requirements update
* updates requirements.txt
* drops debug-toolbar
2024-06-14 07:42:17 +02:00
c9aeb393b5 Merge pull request '# Comment card' (#406) from comment_card_improvement into master
Reviewed-on: #406
2024-05-21 14:43:49 +02:00
6df46e7642 # Comment card
* adds proper line break rendering in comment card
2024-05-21 14:42:49 +02:00
fe366bc568 Merge pull request '# 404 Extend API' (#405) from 404_Extend_API_shared_acces into master
Reviewed-on: #405
2024-05-21 11:55:20 +02:00
a9f04a28c1 # 404 Extend API
* extends API shared record access with team based sharing
2024-05-21 11:54:06 +02:00
8c9f4888dd Merge pull request '# OAuth fix' (#402) from oauth_https_fix into master
Reviewed-on: #402
2024-05-17 10:59:02 +02:00
5c727b2eaa # OAuth fix
* fixes bug in deployment environment due to http/s usage in url
2024-05-17 10:56:33 +02:00
76b2a78fe2 Merge pull request '# Fix' (#400) from oauth_https_fix into master
Reviewed-on: #400
2024-05-17 07:54:02 +02:00
86db08fca0 # Fix
* fixes bug where oauth requests did not use https in dockered deployment environment
2024-05-17 07:49:46 +02:00
fe1dce6440 Merge pull request '# Hotfix' (#398) from 395_OAuth2_refactoring into master
Reviewed-on: #398
2024-05-16 17:37:38 +02:00
a5e6f5a1db # Hotfix
* changes randomly created code verifier into static one to avoid authentication conflicts on multi process deployment (where each process generates an own verifier...)
2024-05-16 17:37:19 +02:00
78e9cbab71 Merge pull request '395_OAuth2_refactoring' (#396) from 395_OAuth2_refactoring into master
Reviewed-on: #396
2024-05-16 15:19:19 +02:00
572348f9f1 # OAuth Propagation
* adds user propagation without django-simple-sso
2024-05-10 10:40:19 +02:00
8ff3cb9adc # OAuth migrations
* adds migrations for storing OAuthToken
* adds OAuthToken model
* adds OAuthToken admin
* adds user migration for Fkey relation to OAuthToken
2024-04-30 14:56:48 +02:00
f135008447 # OAuth refactoring code
* refactors code
2024-04-29 12:27:07 +02:00
94b7f3ad70 # OAuth requirements
* updates requirements.txt
2024-04-29 12:14:15 +02:00
d69bab36da # WIP: OAuth draft implementation
* first working client implementation of oauth workflow for logging in users
2024-04-29 12:07:06 +02:00
fa86cc142f Merge pull request 'requirements_update' (#394) from requirements_update into master
Reviewed-on: SGD-Nord/konova#394
2024-04-12 08:08:00 +02:00
6523891703 # Itsdangerous update
* adds itsdangerous package update
2024-04-12 07:51:18 +02:00
18f590f4a6 # Requirements update
* updates requirements.txt
2024-04-12 07:51:17 +02:00
b441518334 # Env
* updates env.sample
2024-04-03 13:45:52 +02:00
1a80912960 # Environment
* refactors settings into env usage
* adds proxy usage for schneider parcel fetching (using public web address instead of internal ip address)
2024-04-03 13:45:08 +02:00
04dc7fcd30 # Admin backends
* disables certain admin backends
* adds proper ordering to server message admin overview
2024-04-03 08:29:19 +02:00
09546212b9 # Admin button
* adds button for easier admin backend access
2024-04-03 08:26:00 +02:00
b1cd7dee40 # JSON Decode error catch
* adds error catching on wfs parcel resolving
2024-03-15 09:10:06 +01:00
c772e1de06 Merge remote-tracking branch 'origin/master' 2024-03-12 10:32:17 +01:00
4332a750d1 # Message rendering
* adds icons to message danger, info and success rendering
2024-03-12 10:32:05 +01:00
47279dd55d # Requirements
* updates requirements.txt
2024-03-11 08:12:21 +01:00
e2eb0ecbb0 HOTFIX
* fixes bug where rectangular geometry results in an error during geometry complexity calculation
2024-02-29 18:37:53 +01:00
be5b8457a6 HOTFIX
* downgrades package qrcode from 7.4.2 to 7.3.1. Further details can be found in https://github.com/lincolnloop/python-qrcode/issues/353
2024-02-22 18:18:24 +01:00
72f1d80261 # HOTFIX
* drops need for authentication for calculated parcels of an entry (reports are publicly available -> does not need auth!)
2024-02-21 18:31:43 +01:00
df55c16498 Merge pull request '# 382 - Redirect as 404' (#386) from 382_Custom_response_for_Validation_Error into master
Reviewed-on: SGD-Nord/konova#386
2024-02-16 10:16:05 +01:00
11cc8b6766 # 382 - Redirect as 404
* extends 404 template (user should check the URL)
* introduces new decorator "uuid_required" which performs a check on a given 'uuid' or 'id' parameter
    * throws a Http404 exception --> redirect to 404 template instead of 500 error template
2024-02-16 10:13:43 +01:00
0b5691f501 Merge pull request 'geom_parcel_improvements' (#384) from geom_parcel_improvements into master
Reviewed-on: SGD-Nord/konova#384
2024-02-16 08:44:38 +01:00
d76a1fc85f # Fixes
* drops unused methods
* fixes typos
* updates comments
* drops unused model attribute
2024-02-16 08:41:03 +01:00
476447c621 # CONN_MAX_AGE
* dropping conn_max_age due to problems with usage in gunicorn
2024-02-16 08:23:14 +01:00
2b94e537ae # Typo
* fixes typo
2024-02-16 08:14:42 +01:00
c06088a260 # Renaming
* renames a method and fixes doc string
2024-02-16 08:13:10 +01:00
4fc15f6a9d # Optimizations and fixes
* drops identifier handling on all edit-forms (identifier editing has been disabled on the frontend for a while now)
* updates test cases
* updates form caption for checking and recording action (less intimidating)
* optimizes district column width
* fixes bug on frontend parcel fetching on detail view
* adds extended tooltip for title column on index tables
* retraslates 'Law' to 'Rechtsgrundlage'
2024-02-08 07:31:19 +01:00
cf90f9710c # Geom parcel performance improvement
* refactors parcel calculating, resulting in 1.3-1.6x better performance
* optimizes parcel fetching view
2024-01-17 11:22:21 +01:00
8bcccb4685 # WIP: Performance boost parcel calculation
* improves handling of parcel calculation (speed up by ~30%)
* ToDo: Clean up code
2024-01-16 07:57:29 +01:00
50bd6feb89 # Issue #381
* adds another validity check to SimpleGeomForm (is_size_valid) to make sure the area of the entered geometry is somehow rational (>= 1m²)
* optimizes performance of django command sanitize_db
* extends Geometry model with two new attributes, holding timestamps when a parcel calculation has been started and ended
* finally drops unused update_parcel_wfs in favor of update_parcels in Geometry model
* refactors update_parcel method
* adds geometry buffer fallback in schneider/fetcher.py to avoid emptying of geometries when parcels shall be fetched
* finally removes utils/wfs/spatial.py
* extends GeomParcelsView according to #381
* updates translations
* removes redundant psycopg2-binary requirement
2024-01-09 13:11:04 +01:00
d911f4a3a3 Merge pull request 'django_5' (#378) from django_5 into master
Reviewed-on: SGD-Nord/konova#378
2024-01-05 09:37:18 +01:00
ab5cdfbcbf Django5
* updates requirements.txt
2024-01-05 09:34:18 +01:00
814afb39ae Netgis map client fix
* fixes performance issue on drawing when WFS is activated
2023-12-28 15:12:06 +01:00
c2ef5160a3 Django5
* updates requirements
* adds Django5 related migration and setting (DEFAULT_AUTO_FIELD)
2023-12-19 08:54:42 +01:00
e63fb6b8b6 Merge pull request 'EGON document fix' (#374) from 373_EGON_document into master
Reviewed-on: SGD-Nord/konova#374
2023-12-13 13:37:23 +01:00
425ebe54e6 EGON document fix
* fixes bug where documents have not been serialized properly into a single xml list
2023-12-13 13:34:46 +01:00
137 changed files with 2036 additions and 2842 deletions

48
.env.sample Normal file
View File

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

1
.gitignore vendored
View File

@ -3,3 +3,4 @@
/.idea/ /.idea/
/.coverage /.coverage
/htmlcov/ /htmlcov/
/.env

View File

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

View File

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

View File

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

View File

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

View File

@ -6,6 +6,7 @@ Created on: 24.01.22
""" """
from django.db import transaction from django.db import transaction
from django.db.models import Q
from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin from api.utils.serializer.v1.serializer import AbstractModelAPISerializerV1, AbstractCompensationAPISerializerV1Mixin
from compensation.models import Compensation from compensation.models import Compensation
@ -21,8 +22,10 @@ class CompensationAPISerializerV1(AbstractModelAPISerializerV1, AbstractCompensa
def prepare_lookup(self, id, user): def prepare_lookup(self, id, user):
super().prepare_lookup(id, user) super().prepare_lookup(id, user)
del self.lookup["users__in"] self.shared_lookup = Q(
self.lookup["intervention__users__in"] = [user] Q(intervention__users__in=[user]) |
Q(intervention__teams__in=user.shared_teams)
)
def intervention_to_json(self, entry): def intervention_to_json(self, entry):
return { return {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -148,7 +148,7 @@ class CompensationActionAdmin(admin.ModelAdmin):
admin.site.register(Compensation, CompensationAdmin) admin.site.register(Compensation, CompensationAdmin)
admin.site.register(EcoAccount, EcoAccountAdmin) admin.site.register(EcoAccount, EcoAccountAdmin)
admin.site.register(EcoAccountDeduction, EcoAccountDeductionAdmin) #admin.site.register(EcoAccountDeduction, EcoAccountDeductionAdmin)
# For a more cleaner admin interface these rarely used admin views are not important for deployment # For a more cleaner admin interface these rarely used admin views are not important for deployment
#admin.site.register(Payment, PaymentAdmin) #admin.site.register(Payment, PaymentAdmin)

View File

@ -213,7 +213,6 @@ class EditCompensationForm(NewCompensationForm):
action = UserActionLogEntry.get_edited_action(user) action = UserActionLogEntry.get_edited_action(user)
# Fetch data from cleaned POST values # Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
intervention = self.cleaned_data.get("intervention", None) intervention = self.cleaned_data.get("intervention", None)
is_cef = self.cleaned_data.get("is_cef", None) is_cef = self.cleaned_data.get("is_cef", None)
@ -221,7 +220,6 @@ class EditCompensationForm(NewCompensationForm):
is_pik = self.cleaned_data.get("is_pik", None) is_pik = self.cleaned_data.get("is_pik", None)
comment = self.cleaned_data.get("comment", None) comment = self.cleaned_data.get("comment", None)
self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.intervention = intervention self.instance.intervention = intervention
self.instance.is_cef = is_cef self.instance.is_cef = is_cef

View File

@ -192,7 +192,6 @@ class EditEcoAccountForm(NewEcoAccountForm):
def save(self, user: User, geom_form: SimpleGeomForm): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
# Fetch data from cleaned POST values # Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
registration_date = self.cleaned_data.get("registration_date", None) registration_date = self.cleaned_data.get("registration_date", None)
handler_type = self.cleaned_data.get("handler_type", None) handler_type = self.cleaned_data.get("handler_type", None)
@ -219,7 +218,6 @@ class EditEcoAccountForm(NewEcoAccountForm):
self.instance.legal.save() self.instance.legal.save()
# Update main oject data # Update main oject data
self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.deductable_surface = surface self.instance.deductable_surface = surface
self.instance.comment = comment self.instance.comment = comment

View File

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

View File

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

View File

@ -315,7 +315,6 @@ class Compensation(AbstractCompensation, CEFMixin, CoherenceMixin, PikMixin):
def get_detail_url_absolute(self): def get_detail_url_absolute(self):
return BASE_URL + self.get_detail_url() return BASE_URL + self.get_detail_url()
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
if self.identifier is None or len(self.identifier) == 0: if self.identifier is None or len(self.identifier) == 0:
# Create new identifier is none was given # Create new identifier is none was given

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -61,7 +61,7 @@
<td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td> <td class="align-middle">{{ deduction.surface|floatformat:2|intcomma }} m²</td>
<td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td> <td class="align-middle">{{ deduction.created.timestamp|default_if_none:""|naturalday}}</td>
<td class="align-middle float-right"> <td class="align-middle float-right">
{% if is_default_member and has_access or is_default_member and user in deduction.intervention.shared_users %} {% if is_default_member and is_entry_shared or is_default_member and user in deduction.intervention.shared_users %}
<button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}"> <button data-form-url="{% url 'compensation:acc:edit-deduction' deduction.account.id deduction.id %}" class="btn btn-default btn-modal" title="{% trans 'Edit Deduction' %}">
{% fa5_icon 'edit' %} {% fa5_icon 'edit' %}
</button> </button>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -82,6 +82,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
url = reverse("compensation:acc:edit", args=(self.eco_account.id,)) url = reverse("compensation:acc:edit", args=(self.eco_account.id,))
pre_edit_log_count = self.eco_account.log.count() pre_edit_log_count = self.eco_account.log.count()
old_identifier = self.eco_account.identifier
new_title = self.create_dummy_string() new_title = self.create_dummy_string()
new_identifier = self.create_dummy_string() new_identifier = self.create_dummy_string()
new_comment = self.create_dummy_string() new_comment = self.create_dummy_string()
@ -114,7 +115,6 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
check_on_elements = { check_on_elements = {
self.eco_account.title: new_title, self.eco_account.title: new_title,
self.eco_account.identifier: new_identifier,
self.eco_account.deductable_surface: test_deductable_surface, self.eco_account.deductable_surface: test_deductable_surface,
self.eco_account.deductable_rest: test_deductable_surface - deductions_surface, self.eco_account.deductable_rest: test_deductable_surface - deductions_surface,
self.eco_account.comment: new_comment, self.eco_account.comment: new_comment,
@ -123,6 +123,7 @@ class EcoAccountWorkflowTestCase(BaseWorkflowTestCase):
for k, v in check_on_elements.items(): for k, v in check_on_elements.items():
self.assertEqual(k, v) self.assertEqual(k, v)
self.assertEqual(self.eco_account.identifier, old_identifier)
self.assert_equal_geometries(self.eco_account.geometry.geom, new_geometry) self.assert_equal_geometries(self.eco_account.geometry.geom, new_geometry)
# Expect logs to be set # Expect logs to be set

View File

@ -19,7 +19,8 @@ from compensation.models import Compensation
from compensation.tables.compensation import CompensationTable from compensation.tables.compensation import CompensationTable
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
@ -200,6 +201,7 @@ def edit_view(request: HttpRequest, id: str):
@login_required @login_required
@any_group_check @any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str): def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation """ Renders a detail view for a compensation
@ -257,7 +259,7 @@ def detail_view(request: HttpRequest, id: str):
"last_checked_tooltip": last_checked_tooltip, "last_checked_tooltip": last_checked_tooltip,
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels, "parcels": parcels,
"has_access": is_data_shared, "is_entry_shared": is_data_shared,
"actions": actions, "actions": actions,
"before_states": before_states, "before_states": before_states,
"after_states": after_states, "after_states": after_states,

View File

@ -67,7 +67,7 @@ def report_view(request: HttpRequest, id: str):
"img": qrcode_img_lanis, "img": qrcode_img_lanis,
"url": qrcode_lanis_url, "url": qrcode_lanis_url,
}, },
"has_access": False, # disables action buttons during rendering "is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states, "before_states": before_states,
"after_states": after_states, "after_states": after_states,
"geom_form": geom_form, "geom_form": geom_form,

View File

@ -17,7 +17,8 @@ from compensation.forms.eco_account import EditEcoAccountForm, NewEcoAccountForm
from compensation.models import EcoAccount from compensation.models import EcoAccount
from compensation.tables.eco_account import EcoAccountTable from compensation.tables.eco_account import EcoAccountTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal from konova.decorators import shared_access_required, default_group_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP from konova.settings import ETS_GROUP, DEFAULT_GROUP, ZB_GROUP
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
@ -177,6 +178,7 @@ def edit_view(request: HttpRequest, id: str):
@login_required @login_required
@any_group_check @any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str): def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for a compensation """ Renders a detail view for a compensation
@ -235,7 +237,7 @@ def detail_view(request: HttpRequest, id: str):
"obj": acc, "obj": acc,
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels, "parcels": parcels,
"has_access": is_data_shared, "is_entry_shared": is_data_shared,
"before_states": before_states, "before_states": before_states,
"after_states": after_states, "after_states": after_states,
"sum_before_states": sum_before_states, "sum_before_states": sum_before_states,

View File

@ -73,7 +73,7 @@ def report_view(request: HttpRequest, id: str):
"img": qrcode_img_lanis, "img": qrcode_img_lanis,
"url": qrcode_lanis_url, "url": qrcode_lanis_url,
}, },
"has_access": False, # disables action buttons during rendering "is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states, "before_states": before_states,
"after_states": after_states, "after_states": after_states,
"geom_form": geom_form, "geom_form": geom_form,

View File

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

View File

@ -133,7 +133,6 @@ class EditEmaForm(NewEmaForm):
def save(self, user: User, geom_form: SimpleGeomForm): def save(self, user: User, geom_form: SimpleGeomForm):
with transaction.atomic(): with transaction.atomic():
# Fetch data from cleaned POST values # Fetch data from cleaned POST values
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
handler_type = self.cleaned_data.get("handler_type", None) handler_type = self.cleaned_data.get("handler_type", None)
handler_detail = self.cleaned_data.get("handler_detail", None) handler_detail = self.cleaned_data.get("handler_detail", None)
@ -154,7 +153,6 @@ class EditEmaForm(NewEmaForm):
self.instance.responsible.save() self.instance.responsible.save()
# Update main oject data # Update main oject data
self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.comment = comment self.instance.comment = comment
self.instance.is_pik = is_pik self.instance.is_pik = is_pik

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -130,7 +130,7 @@ class EditEmaFormTestCase(BaseTestCase):
self.assertIsNotNone(obj.responsible.handler) self.assertIsNotNone(obj.responsible.handler)
self.assertEqual(obj.responsible.conservation_office, data["conservation_office"]) self.assertEqual(obj.responsible.conservation_office, data["conservation_office"])
self.assertEqual(obj.responsible.conservation_file_number, data["conservation_file_number"]) self.assertEqual(obj.responsible.conservation_file_number, data["conservation_file_number"])
self.assertEqual(obj.identifier, data["identifier"]) self.assertNotEqual(obj.identifier, data["identifier"], msg="Identifier editable via form!")
self.assertEqual(obj.comment, data["comment"]) self.assertEqual(obj.comment, data["comment"])
last_log = obj.log.first() last_log = obj.log.first()

View File

@ -17,7 +17,8 @@ from ema.forms import NewEmaForm, EditEmaForm
from ema.models import Ema from ema.models import Ema
from ema.tables import EmaTable from ema.tables import EmaTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal from konova.decorators import shared_access_required, conservation_office_group_required, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
@ -124,6 +125,7 @@ def new_id_view(request: HttpRequest):
@login_required @login_required
@uuid_required
def detail_view(request: HttpRequest, id: str): def detail_view(request: HttpRequest, id: str):
""" Renders the detail view of an EMA """ Renders the detail view of an EMA
@ -140,7 +142,7 @@ def detail_view(request: HttpRequest, id: str):
geom_form = SimpleGeomForm(instance=ema) geom_form = SimpleGeomForm(instance=ema)
parcels = ema.get_underlying_parcels() parcels = ema.get_underlying_parcels()
_user = request.user _user = request.user
is_data_shared = ema.is_shared_with(_user) is_entry_shared = ema.is_shared_with(_user)
# Order states according to surface # Order states according to surface
before_states = ema.before_states.all().order_by("-surface") before_states = ema.before_states.all().order_by("-surface")
@ -165,7 +167,7 @@ def detail_view(request: HttpRequest, id: str):
"obj": ema, "obj": ema,
"geom_form": geom_form, "geom_form": geom_form,
"parcels": parcels, "parcels": parcels,
"has_access": is_data_shared, "is_entry_shared": is_entry_shared,
"before_states": before_states, "before_states": before_states,
"after_states": after_states, "after_states": after_states,
"sum_before_states": sum_before_states, "sum_before_states": sum_before_states,

View File

@ -67,7 +67,7 @@ def report_view(request:HttpRequest, id: str):
"img": qrcode_img_lanis, "img": qrcode_img_lanis,
"url": qrcode_lanis_url "url": qrcode_lanis_url
}, },
"has_access": False, # disables action buttons during rendering "is_entry_shared": False, # disables action buttons during rendering
"before_states": before_states, "before_states": before_states,
"after_states": after_states, "after_states": after_states,
"geom_form": geom_form, "geom_form": geom_form,

View File

@ -345,7 +345,6 @@ class EditInterventionForm(NewInterventionForm):
""" """
with transaction.atomic(): with transaction.atomic():
identifier = self.cleaned_data.get("identifier", None)
title = self.cleaned_data.get("title", None) title = self.cleaned_data.get("title", None)
process_type = self.cleaned_data.get("type", None) process_type = self.cleaned_data.get("type", None)
laws = self.cleaned_data.get("laws", None) laws = self.cleaned_data.get("laws", None)
@ -379,7 +378,6 @@ class EditInterventionForm(NewInterventionForm):
self.instance.log.add(user_action) self.instance.log.add(user_action)
self.instance.identifier = identifier
self.instance.title = title self.instance.title = title
self.instance.comment = comment self.instance.comment = comment
self.instance.modified = user_action self.instance.modified = user_action

View File

@ -33,7 +33,7 @@ class CheckModalForm(BaseModalForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Run check") self.form_title = _("Run check")
self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) self.form_caption = _("The necessary control steps have been performed:").format(self.user.first_name, self.user.last_name)
self.valid = False self.valid = False
def _are_deductions_valid(self): def _are_deductions_valid(self):

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -146,17 +146,17 @@ class EgonGmlBuilder:
def _gen_foto(self): def _gen_foto(self):
revoc_docs, regular_docs = self.intervention.get_documents() revoc_docs, regular_docs = self.intervention.get_documents()
docs_list = [ docs_list = {
{ "oneo:Foto": [
"oneo:Foto": { {
"oneo:aufnahmezeitpunkt": doc.date_of_creation.strftime(DEFAULT_DATE_FORMAT), "oneo:aufnahmezeitpunkt": doc.date_of_creation.strftime(DEFAULT_DATE_FORMAT),
"oneo:bemerkung": doc.comment, "oneo:bemerkung": doc.comment,
"oneo:fotoverweis": base64.b64encode(doc.file.read()).decode("utf-8"), "oneo:fotoverweis": base64.b64encode(doc.file.read()).decode("utf-8"),
"oneo:dateiname": doc.file.url.split("/")[-1], "oneo:dateiname": doc.file.url.split("/")[-1],
"oneo:hauptfoto": False, "oneo:hauptfoto": False,
} } for doc in regular_docs
} for doc in regular_docs ]
] }
return docs_list return docs_list
def build_gml(self): def build_gml(self):

View File

@ -16,7 +16,8 @@ from intervention.forms.intervention import EditInterventionForm, NewInterventio
from intervention.models import Intervention from intervention.models import Intervention
from intervention.tables import InterventionTable from intervention.tables import InterventionTable
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import default_group_required, shared_access_required, any_group_check, login_required_modal from konova.decorators import default_group_required, shared_access_required, any_group_check, login_required_modal, \
uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.forms.modals import RemoveModalForm from konova.forms.modals import RemoveModalForm
from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP from konova.settings import DEFAULT_GROUP, ZB_GROUP, ETS_GROUP
@ -39,7 +40,7 @@ def index_view(request: HttpRequest):
""" """
template = "generic_index.html" template = "generic_index.html"
# Filtering by user access is performed in table filter inside of InterventionTableFilter class # Filtering by user access is performed in table filter inside InterventionTableFilter class
interventions = Intervention.objects.filter( interventions = Intervention.objects.filter(
deleted=None, # not deleted deleted=None, # not deleted
).select_related( ).select_related(
@ -128,6 +129,7 @@ def new_id_view(request: HttpRequest):
@login_required @login_required
@any_group_check @any_group_check
@uuid_required
def detail_view(request: HttpRequest, id: str): def detail_view(request: HttpRequest, id: str):
""" Renders a detail view for viewing an intervention's data """ Renders a detail view for viewing an intervention's data
@ -183,7 +185,7 @@ def detail_view(request: HttpRequest, id: str):
"last_checked": last_checked, "last_checked": last_checked,
"last_checked_tooltip": last_checked_tooltip, "last_checked_tooltip": last_checked_tooltip,
"compensations": compensations, "compensations": compensations,
"has_access": is_data_shared, "is_entry_shared": is_data_shared,
"geom_form": geom_form, "geom_form": geom_form,
"is_default_member": _user.in_group(DEFAULT_GROUP), "is_default_member": _user.in_group(DEFAULT_GROUP),
"is_zb_member": _user.in_group(ZB_GROUP), "is_zb_member": _user.in_group(ZB_GROUP),

View File

@ -12,11 +12,13 @@ from django.utils.translation import gettext_lazy as _
from intervention.models import Intervention from intervention.models import Intervention
from konova.contexts import BaseContext from konova.contexts import BaseContext
from konova.decorators import uuid_required
from konova.forms import SimpleGeomForm from konova.forms import SimpleGeomForm
from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER from konova.sub_settings.context_settings import TAB_TITLE_IDENTIFIER
from konova.utils.generators import generate_qr_code from konova.utils.generators import generate_qr_code
@uuid_required
def report_view(request: HttpRequest, id: str): def report_view(request: HttpRequest, id: str):
""" Renders the public report view """ Renders the public report view

View File

@ -1,6 +1,7 @@
import os import os
from celery import Celery from celery import Celery
from konova.sub_settings.django_settings import env
# Set the default Django settings module for the 'celery' program. # Set the default Django settings module for the 'celery' program.
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'konova.settings') os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'konova.settings')
@ -17,7 +18,7 @@ app.config_from_object('django.conf:settings', namespace='CELERY')
app.autodiscover_tasks() app.autodiscover_tasks()
# Declare redis as broker # Declare redis as broker
app.conf.broker_url = 'redis://localhost:6379/0' app.conf.broker_url = f'redis://{env("REDIS_HOST")}:{env.int("REDIS_PORT")}/0'
@app.task(bind=True) @app.task(bind=True)

View File

@ -7,9 +7,11 @@ Created on: 16.11.20
""" """
from functools import wraps from functools import wraps
from uuid import UUID
from bootstrap_modal_forms.mixins import is_ajax from bootstrap_modal_forms.mixins import is_ajax
from django.contrib import messages from django.contrib import messages
from django.http import Http404
from django.shortcuts import redirect, get_object_or_404, render from django.shortcuts import redirect, get_object_or_404, render
from django.urls import reverse from django.urls import reverse
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
@ -171,3 +173,20 @@ def login_required_modal(function):
return render(request, template, context) return render(request, template, context)
return function(request, *args, **kwargs) return function(request, *args, **kwargs)
return wrap return wrap
def uuid_required(function):
"""
Checks whether the given input is a valid UUID
"""
@wraps(function)
def wrap(request, *args, **kwargs):
uuid = kwargs.get("uuid", None) or kwargs.get("id", None)
try:
uuid = UUID(uuid)
except ValueError:
raise Http404(
"Invalid UUID"
)
return function(request, *args, **kwargs)
return wrap

View File

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

View File

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

View File

@ -98,12 +98,14 @@ class SimpleGeomForm(BaseForm):
if g.geom_type not in accepted_ogr_types: if g.geom_type not in accepted_ogr_types:
self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered.")) self.add_error("geom", _("Only surfaces allowed. Points or lines must be buffered."))
is_valid = False is_valid &= False
return is_valid return is_valid
is_valid &= self.__is_area_valid(g)
polygon = Polygon.from_ewkt(g.ewkt) polygon = Polygon.from_ewkt(g.ewkt)
is_valid = polygon.valid is_valid &= polygon.valid
if not is_valid: if not polygon.valid:
self.add_error("geom", polygon.valid_reason) self.add_error("geom", polygon.valid_reason)
return is_valid return is_valid
@ -137,6 +139,24 @@ class SimpleGeomForm(BaseForm):
return num_vertices <= GEOM_MAX_VERTICES 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)
if not is_area_valid:
self.add_error(
"geom",
_("Geometry must be greater than 1m². Currently is {}").format(
float(geom.area)
)
)
return is_area_valid
def __simplify_geometry(self, geom, max_vert: int): def __simplify_geometry(self, geom, max_vert: int):
""" Simplifies a geometry """ Simplifies a geometry

View File

@ -27,7 +27,7 @@ class RecordModalForm(BaseModalForm):
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs) super().__init__(*args, **kwargs)
self.form_title = _("Record data") self.form_title = _("Record data")
self.form_caption = _("I, {} {}, confirm that all necessary control steps have been performed by myself.").format(self.user.first_name, self.user.last_name) self.form_caption = _("The necessary control steps have been performed:").format(self.user.first_name, self.user.last_name)
# Disable automatic w-100 setting for this type of modal form. Looks kinda strange # Disable automatic w-100 setting for this type of modal form. Looks kinda strange
self.fields["confirm"].widget.attrs["class"] = "" self.fields["confirm"].widget.attrs["class"] = ""

View File

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

View File

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

View File

@ -61,14 +61,24 @@ class Command(BaseKonovaCommand):
action=UserAction.CREATED action=UserAction.CREATED
) )
intervention_log_entries_ids = self.get_all_log_entries_ids(Intervention) EIV_log_entries_ids = self.get_all_log_entries_ids(Intervention)
attached_log_entries_id = intervention_log_entries_ids.union( self._write_warning(f" EIV: {EIV_log_entries_ids.count()} attached log entries")
self.get_all_log_entries_ids(Compensation), KOM_log_entries_ids = self.get_all_log_entries_ids(Compensation)
self.get_all_log_entries_ids(EcoAccount), self._write_warning(f" KOM: {KOM_log_entries_ids.count()} attached log entries")
self.get_all_log_entries_ids(Ema), OEK_log_entries_ids = self.get_all_log_entries_ids(EcoAccount)
) self._write_warning(f" OEK: {OEK_log_entries_ids.count()} attached log entries")
EMA_log_entries_ids = self.get_all_log_entries_ids(Ema)
self._write_warning(f" EMA: {EMA_log_entries_ids.count()} attached log entries")
unattached_log_entries = all_log_entries.exclude(id__in=attached_log_entries_id) unattached_log_entries = all_log_entries.exclude(
id__in=EIV_log_entries_ids
).exclude(
id__in=KOM_log_entries_ids
).exclude(
id__in=OEK_log_entries_ids
).exclude(
id__in=EMA_log_entries_ids
)
num_entries = unattached_log_entries.count() num_entries = unattached_log_entries.count()
if num_entries > 0: if num_entries > 0:
@ -108,13 +118,20 @@ class Command(BaseKonovaCommand):
self._write_warning("=== Sanitize compensation actions ===") self._write_warning("=== Sanitize compensation actions ===")
all_actions = CompensationAction.objects.all() all_actions = CompensationAction.objects.all()
compensation_action_ids = self.get_all_action_ids(Compensation) kom_action_ids = self.get_all_action_ids(Compensation)
attached_action_ids = compensation_action_ids.union( self._write_warning(f" KOM: {kom_action_ids.count()} attached actions")
self.get_all_action_ids(EcoAccount), oek_action_ids = self.get_all_action_ids(EcoAccount)
self.get_all_action_ids(Ema), self._write_warning(f" OEK: {oek_action_ids.count()} attached actions")
) ema_action_ids = self.get_all_action_ids(Ema)
self._write_warning(f" EMA: {ema_action_ids.count()} attached actions")
unattached_actions = all_actions.exclude(id__in=attached_action_ids) unattached_actions = all_actions.exclude(
id__in=kom_action_ids
).exclude(
id__in=oek_action_ids
).exclude(
id__in=ema_action_ids
)
num_entries = unattached_actions.count() num_entries = unattached_actions.count()
if num_entries > 0: if num_entries > 0:
@ -125,7 +142,7 @@ class Command(BaseKonovaCommand):
self._write_success("No unattached actions found.") self._write_success("No unattached actions found.")
self._break_line() self._break_line()
def get_all_deadline_ids(self, cls): def _get_all_deadline_ids(self, cls):
""" Getter for all deadline ids of a model """ Getter for all deadline ids of a model
Args: Args:
@ -154,13 +171,20 @@ class Command(BaseKonovaCommand):
self._write_warning("=== Sanitize deadlines ===") self._write_warning("=== Sanitize deadlines ===")
all_deadlines = Deadline.objects.all() all_deadlines = Deadline.objects.all()
compensation_deadline_ids = self.get_all_deadline_ids(Compensation) kom_deadline_ids = self._get_all_deadline_ids(Compensation)
attached_deadline_ids = compensation_deadline_ids.union( self._write_warning(f" KOM: {kom_deadline_ids.count()} attached deadlines")
self.get_all_deadline_ids(EcoAccount), oek_deadline_ids = self._get_all_deadline_ids(EcoAccount)
self.get_all_deadline_ids(Ema), self._write_warning(f" OEK: {kom_deadline_ids.count()} attached deadlines")
) ema_deadline_ids = self._get_all_deadline_ids(Ema)
self._write_warning(f" EMA: {kom_deadline_ids.count()} attached deadlines")
unattached_deadlines = all_deadlines.exclude(id__in=attached_deadline_ids) unattached_deadlines = all_deadlines.exclude(
id__in=kom_deadline_ids
).exclude(
id__in=oek_deadline_ids
).exclude(
id__in=ema_deadline_ids
)
num_entries = unattached_deadlines.count() num_entries = unattached_deadlines.count()
if num_entries > 0: if num_entries > 0:
@ -171,7 +195,7 @@ class Command(BaseKonovaCommand):
self._write_success("No unattached deadlines found.") self._write_success("No unattached deadlines found.")
self._break_line() self._break_line()
def get_all_geometry_ids(self, cls): def _get_all_geometry_ids(self, cls):
""" Getter for all geometry ids of a model """ Getter for all geometry ids of a model
Args: Args:
@ -200,14 +224,24 @@ class Command(BaseKonovaCommand):
self._write_warning("=== Sanitize geometries ===") self._write_warning("=== Sanitize geometries ===")
all_geometries = Geometry.objects.all() all_geometries = Geometry.objects.all()
compensation_geometry_ids = self.get_all_geometry_ids(Compensation) kom_geometry_ids = self._get_all_geometry_ids(Compensation)
attached_geometry_ids = compensation_geometry_ids.union( self._write_warning(f" KOM: {kom_geometry_ids.count()} attached geometries")
self.get_all_geometry_ids(Intervention), eiv_geometry_ids = self._get_all_geometry_ids(Intervention)
self.get_all_geometry_ids(EcoAccount), self._write_warning(f" EIV: {eiv_geometry_ids.count()} attached geometries")
self.get_all_geometry_ids(Ema), oek_geometry_ids = self._get_all_geometry_ids(EcoAccount)
) self._write_warning(f" OEK: {oek_geometry_ids.count()} attached geometries")
ema_geometry_ids = self._get_all_geometry_ids(Ema)
self._write_warning(f" EMA: {ema_geometry_ids.count()} attached geometries")
unattached_geometries = all_geometries.exclude(id__in=attached_geometry_ids) unattached_geometries = all_geometries.exclude(
id__in=kom_geometry_ids
).exclude(
id__in=eiv_geometry_ids
).exclude(
id__in=oek_geometry_ids
).exclude(
id__in=ema_geometry_ids
)
num_entries = unattached_geometries.count() num_entries = unattached_geometries.count()
if num_entries > 0: if num_entries > 0:
@ -218,7 +252,7 @@ class Command(BaseKonovaCommand):
self._write_success("No unattached geometries found.") self._write_success("No unattached geometries found.")
self._break_line() self._break_line()
def get_all_state_ids(self, cls): def _get_all_state_ids(self, cls):
""" Getter for all states (before and after) of a class """ Getter for all states (before and after) of a class
Args: Args:
@ -254,14 +288,19 @@ class Command(BaseKonovaCommand):
""" """
self._write_warning("=== Sanitize compensation states ===") self._write_warning("=== Sanitize compensation states ===")
all_states = CompensationState.objects.all() all_states = CompensationState.objects.all()
compensation_state_ids = self.get_all_state_ids(Compensation)
account_state_ids = self.get_all_state_ids(EcoAccount) kom_state_ids = self._get_all_state_ids(Compensation)
ema_state_ids = self.get_all_state_ids(Ema) oek_state_ids = self._get_all_state_ids(EcoAccount)
attached_state_ids = compensation_state_ids.union(account_state_ids, ema_state_ids) ema_state_ids = self._get_all_state_ids(Ema)
unattached_states = all_states.exclude( unattached_states = all_states.exclude(
id__in=attached_state_ids id__in=kom_state_ids
).exclude(
id__in=oek_state_ids
).exclude(
id__in=ema_state_ids
) )
num_unattached_states = unattached_states.count() num_unattached_states = unattached_states.count()
if num_unattached_states > 0: if num_unattached_states > 0:
self._write_error(f"Found {num_unattached_states} unused compensation states. Delete now...") self._write_error(f"Found {num_unattached_states} unused compensation states. Delete now...")

View File

@ -0,0 +1,54 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 18.06.24
"""
from django.db.models import QuerySet
from intervention.models import Intervention
from konova.management.commands.setup import BaseKonovaCommand
class Command(BaseKonovaCommand):
help = "Send specific intervention entries to EGON if there are any payments on them"
def add_arguments(self, parser):
try:
parser.add_argument("--intervention-ids", type=str)
except ValueError as e:
self._write_error(f"Argument error: {e}")
exit(-1)
def __handle_arguments(self, options):
self.intervention_ids = options["intervention_ids"] or ""
self.intervention_ids = self.intervention_ids.split(",")
self.intervention_ids = [x.strip() for x in self.intervention_ids]
def handle(self, *args, **options):
try:
self.__handle_arguments(options)
interventions = self.get_interventions()
self.process_egon_sending(interventions)
except KeyboardInterrupt:
self._break_line()
exit(-1)
def get_interventions(self) -> QuerySet:
"""
Getter for interventions, defined by parameter 'intervention-ids'
Returns:
interventions (QuerySet): The interventions
"""
interventions = Intervention.objects.filter(
id__in=self.intervention_ids,
)
self._write_success(f"... Found {interventions.count()} interventions")
return interventions
def process_egon_sending(self, interventions: QuerySet):
for intervention in interventions:
intervention.send_data_to_egon()
self._write_warning(f"... {intervention.identifier} has been sent to EGON (if it has payments)")

View File

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

View File

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

View File

@ -0,0 +1,23 @@
# Generated by Django 5.0.1 on 2024-01-09 10:38
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('konova', '0014_resubmission'),
]
operations = [
migrations.AddField(
model_name='geometry',
name='parcel_update_end',
field=models.DateTimeField(blank=True, db_comment='When the last parcel calculation finished', help_text='When the last parcel calculation finished', null=True),
),
migrations.AddField(
model_name='geometry',
name='parcel_update_start',
field=models.DateTimeField(blank=True, db_comment='When the last parcel calculation started', help_text='When the last parcel calculation started', null=True),
),
]

View File

@ -0,0 +1,17 @@
# Generated by Django 5.0.1 on 2024-02-16 07:34
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('konova', '0015_geometry_parcel_calculation_end_and_more'),
]
operations = [
migrations.RemoveField(
model_name='parcelintersection',
name='calculated_on',
),
]

View File

@ -8,19 +8,31 @@ Created on: 15.11.21
import json import json
from django.contrib.gis.db.models import MultiPolygonField from django.contrib.gis.db.models import MultiPolygonField
from django.core.exceptions import ObjectDoesNotExist, MultipleObjectsReturned
from django.db import models, transaction from django.db import models, transaction
from django.utils import timezone from django.utils import timezone
from konova.models import BaseResource, UuidModel from konova.models import BaseResource, UuidModel
from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP from konova.sub_settings.lanis_settings import DEFAULT_SRID_RLP
from konova.utils.schneider.fetcher import ParcelFetcher from konova.utils.schneider.fetcher import ParcelFetcher
from konova.utils.wfs.spatial import ParcelWFSFetcher
class Geometry(BaseResource): class Geometry(BaseResource):
""" """
Geometry model Geometry model
""" """
parcel_update_start = models.DateTimeField(
blank=True,
null=True,
db_comment="When the last parcel calculation started",
help_text="When the last parcel calculation started"
)
parcel_update_end = models.DateTimeField(
blank=True,
null=True,
db_comment="When the last parcel calculation finished",
help_text="When the last parcel calculation finished",
)
geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID_RLP) geom = MultiPolygonField(null=True, blank=True, srid=DEFAULT_SRID_RLP)
def __str__(self): def __str__(self):
@ -109,82 +121,14 @@ class Geometry(BaseResource):
objs += set_objs objs += set_objs
return objs return objs
@transaction.atomic def get_data_object(self):
def update_parcels_wfs(self):
""" Updates underlying parcel information using the WFS of LVermGeo
Returns:
""" """
from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup Getter for the specific data object which is related to this geometry
"""
if self.geom.empty: objs = self.get_data_objects()
# Nothing to do assert (len(objs) <= 1)
return result = objs.pop()
return result
parcel_fetcher = ParcelWFSFetcher(
geometry_id=self.id,
)
typename = "ave:Flurstueck"
fetched_parcels = parcel_fetcher.get_features(
typename
)
_now = timezone.now()
underlying_parcels = []
for result in fetched_parcels:
parcel_properties = result["properties"]
# There could be parcels which include the word 'Flur',
# which needs to be deleted and just keep the numerical values
## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE!
flr_val = parcel_properties["flur"].replace("Flur ", "")
district = District.objects.get_or_create(
key=parcel_properties["kreisschl"],
name=parcel_properties["kreis"],
)[0]
municipal = Municipal.objects.get_or_create(
key=parcel_properties["gmdschl"],
name=parcel_properties["gemeinde"],
district=district,
)[0]
parcel_group = ParcelGroup.objects.get_or_create(
key=parcel_properties["gemaschl"],
name=parcel_properties["gemarkung"],
municipal=municipal,
)[0]
flrstck_nnr = parcel_properties['flstnrnen']
if not flrstck_nnr:
flrstck_nnr = None
flrstck_zhlr = parcel_properties['flstnrzae']
if not flrstck_zhlr:
flrstck_zhlr = None
parcel_obj = Parcel.objects.get_or_create(
district=district,
municipal=municipal,
parcel_group=parcel_group,
flr=flr_val,
flrstck_nnr=flrstck_nnr,
flrstck_zhlr=flrstck_zhlr,
)[0]
parcel_obj.district = district
parcel_obj.updated_on = _now
parcel_obj.save()
underlying_parcels.append(parcel_obj)
# Update the linked parcels
self.parcels.clear()
self.parcels.set(underlying_parcels)
# Set the calculated_on intermediate field, so this related data will be found on lookups
intersections_without_ts = self.parcelintersection_set.filter(
parcel__in=self.parcels.all(),
calculated_on__isnull=True,
)
for entry in intersections_without_ts:
entry.calculated_on = _now
ParcelIntersection.objects.bulk_update(
intersections_without_ts,
["calculated_on"]
)
def update_parcels(self): def update_parcels(self):
""" Updates underlying parcel information """ Updates underlying parcel information
@ -192,72 +136,152 @@ class Geometry(BaseResource):
Returns: Returns:
""" """
from konova.models import Parcel, District, ParcelIntersection, Municipal, ParcelGroup
if self.geom.empty: if self.geom.empty:
# Nothing to do # Nothing to do
return return
self._set_parcel_update_start_time()
self._perform_parcel_update()
self._set_parcel_update_end_time()
def _perform_parcel_update(self):
"""
Performs the main logic of parcel updating.
"""
from konova.models import Parcel, District, Municipal, ParcelGroup
parcel_fetcher = ParcelFetcher( parcel_fetcher = ParcelFetcher(
geometry=self geometry=self
) )
fetched_parcels = parcel_fetcher.get_parcels() fetched_parcels = parcel_fetcher.get_parcels()
_now = timezone.now() _now = timezone.now()
underlying_parcels = []
districts = {}
municipals = {}
parcel_groups = {}
parcels_to_update = []
parcels_to_create = []
for result in fetched_parcels: for result in fetched_parcels:
# There could be parcels which include the word 'Flur', # There could be parcels which include the word 'Flur',
# which needs to be deleted and just keep the numerical values # which needs to be deleted and just keep the numerical values
## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE! ## THIS CAN BE REMOVED IN THE FUTURE, WHEN 'Flur' WON'T OCCUR ANYMORE!
flr_val = result["flur"].replace("Flur ", "") flr_val = result["flur"].replace("Flur ", "")
district = District.objects.get_or_create(
key=result["kreisschl"],
name=result["kreis"],
)[0]
municipal = Municipal.objects.get_or_create(
key=result["gmdschl"],
name=result["gemeinde"],
district=district,
)[0]
parcel_group = ParcelGroup.objects.get_or_create(
key=result["gemaschl"],
name=result["gemarkung"],
municipal=municipal,
)[0]
flrstck_nnr = result['flstnrnen']
if not flrstck_nnr:
flrstck_nnr = None
flrstck_zhlr = result['flstnrzae']
if not flrstck_zhlr:
flrstck_zhlr = None
parcel_obj = Parcel.objects.get_or_create(
district=district,
municipal=municipal,
parcel_group=parcel_group,
flr=flr_val,
flrstck_nnr=flrstck_nnr,
flrstck_zhlr=flrstck_zhlr,
)[0]
parcel_obj.district = district
parcel_obj.updated_on = _now
parcel_obj.save()
underlying_parcels.append(parcel_obj)
# Update the linked parcels # Get district (cache in dict)
self.parcels.clear() try:
district = districts["kreisschl"]
except KeyError:
district = District.objects.get_or_create(
key=result["kreisschl"],
name=result["kreis"],
)[0]
districts[district.key] = district
# Get municipal (cache in dict)
try:
municipal = municipals["gmdschl"]
except KeyError:
municipal = Municipal.objects.get_or_create(
key=result["gmdschl"],
name=result["gemeinde"],
district=district,
)[0]
municipals[municipal.key] = municipal
# Get parcel group (cache in dict)
try:
parcel_group = parcel_groups["gemaschl"]
except KeyError:
parcel_group = ParcelGroup.objects.get_or_create(
key=result["gemaschl"],
name=result["gemarkung"],
municipal=municipal,
)[0]
parcel_groups[parcel_group.key] = parcel_group
# Preprocess parcel data
flrstck_nnr = result['flstnrnen']
match flrstck_nnr:
case "":
flrstck_nnr = None
flrstck_zhlr = result['flstnrzae']
match flrstck_zhlr:
case "":
flrstck_zhlr = None
try:
# Try to fetch parcel from db. If it already exists, just update timestamp.
parcel_obj = Parcel.objects.get(
district=district,
municipal=municipal,
parcel_group=parcel_group,
flr=flr_val,
flrstck_nnr=flrstck_nnr,
flrstck_zhlr=flrstck_zhlr,
)
parcel_obj.updated_on = _now
parcels_to_update.append(parcel_obj)
except MultipleObjectsReturned:
parcel_obj = Parcel.make_unique(
district=district,
municipal=municipal,
parcel_group=parcel_group,
flr=flr_val,
flrstck_nnr=flrstck_nnr,
flrstck_zhlr=flrstck_zhlr,
)
parcel_obj.updated_on = _now
parcels_to_update.append(parcel_obj)
except ObjectDoesNotExist:
# If not existing, create object but do not commit, yet
parcel_obj = Parcel(
district=district,
municipal=municipal,
parcel_group=parcel_group,
flr=flr_val,
flrstck_nnr=flrstck_nnr,
flrstck_zhlr=flrstck_zhlr,
updated_on=_now,
)
parcels_to_create.append(parcel_obj)
# Create new parcels
Parcel.objects.bulk_create(
parcels_to_create,
batch_size=500
)
# Update existing parcels
Parcel.objects.bulk_update(
parcels_to_update,
[
"updated_on"
],
batch_size=500
)
# Update linking to geometry
parcel_ids = [x.id for x in parcels_to_update] + [x.id for x in parcels_to_create]
underlying_parcels = Parcel.objects.filter(id__in=parcel_ids)
self.parcels.set(underlying_parcels) self.parcels.set(underlying_parcels)
# Set the calculated_on intermediate field, so this related data will be found on lookups @transaction.atomic
intersections_without_ts = self.parcelintersection_set.filter( def _set_parcel_update_start_time(self):
parcel__in=self.parcels.all(), """
calculated_on__isnull=True, Sets the current time for the parcel calculation begin
) """
for entry in intersections_without_ts: self.parcel_update_start = timezone.now()
entry.calculated_on = _now self.parcel_update_end = None
ParcelIntersection.objects.bulk_update( self.save()
intersections_without_ts,
["calculated_on"] @transaction.atomic
) def _set_parcel_update_end_time(self):
"""
Sets the current time for the parcel calculation end
"""
self.parcel_update_end = timezone.now()
self.save()
def get_underlying_parcels(self): def get_underlying_parcels(self):
""" Getter for related parcels and their districts """ Getter for related parcels and their districts
@ -265,9 +289,7 @@ class Geometry(BaseResource):
Returns: Returns:
parcels (QuerySet): The related parcels as queryset parcels (QuerySet): The related parcels as queryset
""" """
parcels = self.parcels.filter( parcels = self.parcels.prefetch_related(
parcelintersection__calculated_on__isnull=False,
).prefetch_related(
"district", "district",
"municipal", "municipal",
).order_by( ).order_by(
@ -292,17 +314,6 @@ class Geometry(BaseResource):
municipals = Municipal.objects.filter(id__in=municipals).order_by("name") municipals = Municipal.objects.filter(id__in=municipals).order_by("name")
return municipals return municipals
def count_underlying_parcels(self):
""" Getter for number of underlying parcels
Returns:
"""
num_parcels = self.parcels.filter(
parcelintersection__calculated_on__isnull=False,
).count()
return num_parcels
def as_feature_collection(self, srid=DEFAULT_SRID_RLP): def as_feature_collection(self, srid=DEFAULT_SRID_RLP):
""" Returns a FeatureCollection structure holding all polygons of the MultiPolygon as single features """ Returns a FeatureCollection structure holding all polygons of the MultiPolygon as single features
@ -337,6 +348,41 @@ class Geometry(BaseResource):
} }
return geojson return geojson
@property
def complexity_factor(self) -> float:
""" Calculates a factor to estimate the complexity of a Geometry
0 = very low complexity
1 = very high complexity
ASSUMPTION:
The envelope is the bounding box of a geometry. If the geometry's area is similar to the area of it's bounding
box, it is considered as rather simple, since it seems to be a closer shape like a simple box.
If the geometry has a very big bounding box, but the geometry's own area is rather small,
compared to the one of the bounding box, the complexity can be higher.
Example:
geometry area similar to bounding box --> geometry / bounding_box ~ 1
geometry area far smaller than bb --> geometry / bounding_box ~ 0
Result is being inverted for better understanding of 'low' and 'high' complexity.
Returns:
complexity_factor (float): The estimated complexity
"""
if self.geom.empty:
return 0
geom_envelope = self.geom.envelope
diff = geom_envelope - self.geom
if diff.area == 0:
complexity_factor = 1
else:
complexity_factor = self.geom.area / diff.area
return complexity_factor
class GeometryConflict(UuidModel): class GeometryConflict(UuidModel):
""" """

View File

@ -672,17 +672,6 @@ class GeoReferencedMixin(models.Model):
result = self.geometry.get_underlying_parcels() result = self.geometry.get_underlying_parcels()
return result return result
def count_underlying_parcels(self):
""" Getter for number of underlying parcels
Returns:
"""
result = 0
if self.geometry is not None:
result = self.geometry.count_underlying_parcels()
return result
def set_geometry_conflict_message(self, request: HttpRequest): def set_geometry_conflict_message(self, request: HttpRequest):
if self.geometry is None: if self.geometry is None:
return request return request

View File

@ -5,7 +5,7 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 16.12.21 Created on: 16.12.21
""" """
from django.db import models from django.db import models, transaction
from konova.models import UuidModel from konova.models import UuidModel
@ -158,21 +158,51 @@ class Parcel(UuidModel):
def __str__(self): def __str__(self):
return f"{self.parcel_group} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}" return f"{self.parcel_group} | {self.flr} | {self.flrstck_zhlr} | {self.flrstck_nnr}"
@classmethod
def make_unique(cls, **kwargs):
""" Checks for duplicates of a Parcel, choose a (now) unique one,
repairs relations for ParcelIntersection and removes duplicates.
Args:
**kwargs ():
Returns:
unique_true (Parcel): The new unique 'true one'
"""
parcel_objs = Parcel.objects.filter(**kwargs)
if not parcel_objs.exists():
return None
# Get one of the found parcels and use it as new 'true one'
unique_parcel = parcel_objs.first()
# separate it from the rest
parcel_objs = parcel_objs.exclude(id=unique_parcel.id)
if not parcel_objs.exists():
# There are no duplicates - all good, just return
return unique_parcel
# Fetch existing intersections, which still point on the duplicated parcels
intersection_objs = ParcelIntersection.objects.filter(
parcel__in=parcel_objs
)
# Change each intersection, so they point on the 'true one' parcel from now on
for intersection in intersection_objs:
intersection.parcel = unique_parcel
intersection.save()
# Remove the duplicated parcels
parcel_objs.delete()
return unique_parcel
class ParcelIntersection(UuidModel): class ParcelIntersection(UuidModel):
""" ParcelIntersection is an intermediary model, which is used to configure the """
ParcelIntersection is an intermediary model, which is used to add extras to the
M2M relation between Parcel and Geometry. M2M relation between Parcel and Geometry.
Based on uuids, we will not have (practically) any problems on outrunning primary keys
and extending the model with calculated_on timestamp, we can 'hide' entries while they
are being recalculated and keep track on the last time they have been calculated this
way.
Please note: The calculated_on describes when the relation between the Parcel and the Geometry
has been established. The updated_on field of Parcel describes when this Parcel has been
changed the last time.
""" """
parcel = models.ForeignKey(Parcel, on_delete=models.CASCADE) parcel = models.ForeignKey(Parcel, on_delete=models.CASCADE)
geometry = models.ForeignKey("konova.Geometry", on_delete=models.CASCADE) geometry = models.ForeignKey("konova.Geometry", on_delete=models.CASCADE)
calculated_on = models.DateTimeField(auto_now_add=True, null=True, blank=True)

View File

@ -18,7 +18,6 @@ from konova.sub_settings.proxy_settings import *
from konova.sub_settings.sso_settings import * from konova.sub_settings.sso_settings import *
from konova.sub_settings.table_settings import * from konova.sub_settings.table_settings import *
from konova.sub_settings.lanis_settings import * from konova.sub_settings.lanis_settings import *
from konova.sub_settings.wfs_parcel_settings import *
from konova.sub_settings.logging_settings import * from konova.sub_settings.logging_settings import *
# Max upload size for POST forms # Max upload size for POST forms
@ -46,4 +45,8 @@ DEFAULT_GROUP = "Default"
ZB_GROUP = "Registration office" ZB_GROUP = "Registration office"
ETS_GROUP = "Conservation office" ETS_GROUP = "Conservation office"
# GEOMETRY
## Max number of allowed vertices. Geometries larger will be simplified until they reach this threshold
GEOM_MAX_VERTICES = 10000 GEOM_MAX_VERTICES = 10000
## Max seconds to wait for a parcel calculation result before a new request will be started (default: 30 minutes)
GEOM_THRESHOLD_RECALCULATION_SECONDS = 60 * 30

View File

@ -1,78 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 17.08.21
"""
from django.http import HttpResponse
from django.urls import re_path
from django.views import View
from django.views.decorators.csrf import csrf_exempt
from itsdangerous import TimedSerializer
from simple_sso.sso_client.client import Client
from user.models import User
class PropagateView(View):
""" View used to receive propagated sso-server user data
"""
client = None
signer = None
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.signer = TimedSerializer(self.client.private_key)
@csrf_exempt
def dispatch(self, request, *args, **kwargs):
return super().dispatch(request, *args, **kwargs)
def post(self, request):
user_data = request.body
user_data = self.signer.loads(user_data)
self.client.build_user(user_data)
return HttpResponse(status=200)
class KonovaSSOClient(Client):
""" Konova specialized derivative of general sso.Client.
Adds some custom behaviour for konova usage.
"""
propagate_view = PropagateView
def get_urls(self):
urls = super().get_urls()
urls += re_path(r'^propagate/$', self.propagate_view.as_view(client=self), name='simple-sso-propagate'),
return urls
def build_user(self, user_data):
""" Creates a user or updates user data
Args:
user_data ():
Returns:
"""
try:
user = User.objects.get(username=user_data['username'])
# Update user data, excluding some changes
skipable_attrs = {
"username",
"is_staff",
"is_superuser",
}
for _attr, _val in user_data.items():
if _attr in skipable_attrs:
continue
setattr(user, _attr, _val)
except User.DoesNotExist:
user = User(**user_data)
user.set_unusable_password()
user.save()
return user

View File

@ -276,3 +276,6 @@ Similar to bootstraps 'shadow-lg'
.tree-label.badge{ .tree-label.badge{
font-size: 90%; font-size: 90%;
} }
.alert{
margin-bottom: 0 !important;
}

View File

@ -10,6 +10,8 @@ For the full list of settings and their values, see
https://docs.djangoproject.com/en/3.1/ref/settings/ https://docs.djangoproject.com/en/3.1/ref/settings/
""" """
import os import os
import environ
from django.utils.translation import gettext_lazy as _ from django.utils.translation import gettext_lazy as _
from django.conf.locale.de import formats as de_formats from django.conf.locale.de import formats as de_formats
@ -24,32 +26,28 @@ BASE_DIR = os.path.dirname(
) )
) )
# Quick-start development settings - unsuitable for production env = environ.Env()
# See https://docs.djangoproject.com/en/3.1/howto/deployment/checklist/ # Take environment variables from .env.dev file
environ.Env.read_env(os.path.join(BASE_DIR, '.env'))
# SECURITY WARNING: keep the secret key used in production secret! # SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = '5=9-)2)h$u9=!zrhia9=lj-2#cpcb8=#$7y+)l$5tto$3q(n_+' SECRET_KEY = env("SECRET_KEY")
# SECURITY WARNING: don't run with debug turned on in production! # SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True DEBUG = env.bool("DEBUG", default=False)
ADMINS = [ ADMINS = [x.split(':') for x in env.list('ADMINS')]
('KSP-Servicestelle', 'ksp-servicestelle@sgdnord.rlp.de'),
]
BASE_URL = "http://localhost:8001" ALLOWED_HOSTS = env.list("ALLOWED_HOSTS")
ALLOWED_HOSTS = [ BASE_URL = env("BASE_URL")
"127.0.0.1",
"localhost",
]
CSRF_TRUSTED_ORIGINS = [ CSRF_TRUSTED_ORIGINS = [
"http://localhost", # not only host but schema (http/s) as well! BASE_URL
] ]
# Authentication settings # Authentication settings
LOGIN_URL = "/login/" LOGIN_URL = "/oauth/login/"
# Session settings # Session settings
SESSION_COOKIE_AGE = 60 * 60 # 60 minutes SESSION_COOKIE_AGE = 60 * 60 # 60 minutes
@ -68,7 +66,6 @@ INSTALLED_APPS = [
'django.contrib.staticfiles', 'django.contrib.staticfiles',
'django.contrib.gis', 'django.contrib.gis',
'django.contrib.humanize', 'django.contrib.humanize',
'simple_sso.sso_server',
'django_tables2', 'django_tables2',
'bootstrap_modal_forms', 'bootstrap_modal_forms',
'fontawesome_5', 'fontawesome_5',
@ -83,10 +80,6 @@ INSTALLED_APPS = [
'analysis', 'analysis',
'api', 'api',
] ]
if DEBUG:
INSTALLED_APPS += [
'debug_toolbar',
]
MIDDLEWARE = [ MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware', 'django.middleware.security.SecurityMiddleware',
@ -98,10 +91,6 @@ MIDDLEWARE = [
'django.contrib.messages.middleware.MessageMiddleware', 'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware', 'django.middleware.clickjacking.XFrameOptionsMiddleware',
] ]
if DEBUG:
MIDDLEWARE += [
"debug_toolbar.middleware.DebugToolbarMiddleware",
]
ROOT_URLCONF = 'konova.urls' ROOT_URLCONF = 'konova.urls'
@ -131,13 +120,14 @@ WSGI_APPLICATION = 'konova.wsgi.application'
DATABASES = { DATABASES = {
'default': { 'default': {
'ENGINE': 'django.contrib.gis.db.backends.postgis', 'ENGINE': 'django.contrib.gis.db.backends.postgis',
'NAME': 'konova', 'NAME': env("DB_NAME"),
'USER': 'postgres', 'USER': env("DB_USER"),
'HOST': '127.0.0.1', 'PASSWORD': env("DB_PASSWORD"),
'PORT': '5432', 'HOST': env("DB_HOST"),
'PORT': env("DB_PORT"),
} }
} }
DEFAULT_AUTO_FIELD = "django.db.models.BigAutoField"
# Password validation # Password validation
# https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators # https://docs.djangoproject.com/en/3.1/ref/settings/#auth-password-validators
@ -201,28 +191,6 @@ STATICFILES_DIRS = [
os.path.join(BASE_DIR, 'templates/map/client/libs'), # NETGIS map client files os.path.join(BASE_DIR, 'templates/map/client/libs'), # NETGIS map client files
] ]
# DJANGO DEBUG TOOLBAR
INTERNAL_IPS = [
"127.0.0.1"
]
DEBUG_TOOLBAR_CONFIG = {
"DISABLE_PANELS": {
'debug_toolbar.panels.versions.VersionsPanel',
'debug_toolbar.panels.timer.TimerPanel',
'debug_toolbar.panels.settings.SettingsPanel',
'debug_toolbar.panels.headers.HeadersPanel',
'debug_toolbar.panels.request.RequestPanel',
'debug_toolbar.panels.sql.SQLPanel',
'debug_toolbar.panels.staticfiles.StaticFilesPanel',
'debug_toolbar.panels.templates.TemplatesPanel',
'debug_toolbar.panels.cache.CachePanel',
'debug_toolbar.panels.signals.SignalsPanel',
'debug_toolbar.panels.logging.LoggingPanel',
'debug_toolbar.panels.redirects.RedirectsPanel',
'debug_toolbar.panels.profiling.ProfilingPanel',
}
}
# 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 !!! # CHANGE_ME !!! ONLY FOR DEVELOPMENT !!!
@ -230,13 +198,10 @@ if DEBUG:
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' # change this to a proper location
DEFAULT_FROM_EMAIL = "service@ksp.de" # 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
EMAIL_HOST = "localhost" EMAIL_HOST = env("SMTP_HOST")
EMAIL_REPLY_TO = "ksp-servicestelle@sgdnord.rlp.de" EMAIL_REPLY_TO = env("REPLY_TO_ADDR")
SUPPORT_MAIL_RECIPIENT = EMAIL_REPLY_TO EMAIL_PORT = env("SMTP_PORT")
EMAIL_PORT = "25"
#EMAIL_HOST_USER = ""
#EMAIL_HOST_PASSWORD = ""
EMAIL_USE_TLS = False EMAIL_USE_TLS = False
EMAIL_USE_SSL = False EMAIL_USE_SSL = False

View File

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

View File

@ -5,12 +5,13 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 31.01.22 Created on: 31.01.22
""" """
from konova.sub_settings.django_settings import env
proxy = "" proxy = env("PROXY")
PROXIES = { PROXIES = {
"http": proxy, "http": proxy,
"https": proxy, "https": proxy,
} }
CLIENT_PROXY_AUTH_USER = "CHANGE_ME" GEOPORTAL_RLP_USER = env("GEOPORTAL_RLP_USER")
CLIENT_PROXY_AUTH_PASSWORD = "CHANGE_ME" GEOPORTAL_RLP_PASSWORD = env("GEOPORTAL_RLP_PASSWORD")

View File

@ -5,7 +5,8 @@ Contact: ksp-servicestelle@sgdnord.rlp.de
Created on: 14.12.22 Created on: 14.12.22
""" """
from konova.sub_settings.django_settings import env
base_url = "http://127.0.0.1:8002" base_url = env("SCHNEIDER_BASE_URL")
auth_header = "auth" auth_header = env("SCHNEIDER_AUTH_HEADER")
auth_header_token = "CHANGE_ME" auth_header_token = env("SCHNEIDER_AUTH_TOKEN")

View File

@ -5,9 +5,16 @@ Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 31.01.22 Created on: 31.01.22
""" """
from konova.sub_settings.django_settings import env
# SSO settings # SSO settings
SSO_SERVER_BASE = "http://127.0.0.1:8000/" SSO_SERVER_BASE = env("SSO_SERVER_BASE_URL")
SSO_SERVER = f"{SSO_SERVER_BASE}sso/" SSO_SERVER = f"{SSO_SERVER_BASE}sso/"
SSO_PRIVATE_KEY = "CHANGE_ME"
SSO_PUBLIC_KEY = "CHANGE_ME" # OAuth settings
OAUTH_CODE_VERIFIER = env("OAUTH_CODE_VERIFIER")
OAUTH_CLIENT_ID = env("OAUTH_CLIENT_ID")
OAUTH_CLIENT_SECRET = env("OAUTH_CLIENT_SECRET")
PROPAGATION_SECRET = env("PROPAGATION_SECRET")

View File

@ -1,12 +0,0 @@
"""
Author: Michel Peltriaux
Organization: Struktur- und Genehmigungsdirektion Nord, Rhineland-Palatinate, Germany
Contact: michel.peltriaux@sgdnord.rlp.de
Created on: 31.01.22
"""
# Parcel WFS settings
PARCEL_WFS_BASE_URL = "https://www.geoportal.rlp.de/registry/wfs/519"
PARCEL_WFS_USER = "ksp"
PARCEL_WFS_PW = "CHANGE_ME"

View File

@ -7,18 +7,12 @@ from django.core.exceptions import ObjectDoesNotExist
@shared_task @shared_task
def celery_update_parcels(geometry_id: str, recheck: bool = True): def celery_update_parcels(geometry_id: str, recheck: bool = True):
from konova.models import Geometry, ParcelIntersection from konova.models import Geometry
try: try:
geom = Geometry.objects.get(id=geometry_id) geom = Geometry.objects.get(id=geometry_id)
objs = geom.parcelintersection_set.all() geom.parcels.clear()
for obj in objs:
obj.calculated_on = None
ParcelIntersection.objects.bulk_update(
objs,
["calculated_on"]
)
geom.update_parcels() geom.update_parcels()
except ObjectDoesNotExist: except ObjectDoesNotExist:
if recheck: if recheck:
sleep(5) sleep(5)

View File

@ -20,7 +20,7 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="scroll-150 font-italic"> <div class="scroll-150 font-italic">
{{obj.comment}} {{obj.comment|linebreaks}}
</div> </div>
</div> </div>
</div> </div>

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